diff --git a/agentci/clotho.go b/agentci/clotho.go index 41ce261..eb70d1b 100644 --- a/agentci/clotho.go +++ b/agentci/clotho.go @@ -2,8 +2,8 @@ package agentci import ( "context" - "strings" + core "dappco.re/go/core" "dappco.re/go/core/scm/jobrunner" ) @@ -45,7 +45,7 @@ func (s *Spinner) DeterminePlan(signal *jobrunner.PipelineSignal, agentName stri } // Protect critical repos with dual-run (Axiom 1). - if signal.RepoName == "core" || strings.Contains(signal.RepoName, "security") { + if signal.RepoName == "core" || core.Contains(signal.RepoName, "security") { return ModeDual } diff --git a/agentci/config.go b/agentci/config.go index a7386b2..eb11308 100644 --- a/agentci/config.go +++ b/agentci/config.go @@ -2,9 +2,9 @@ package agentci import ( - "fmt" "forge.lthn.ai/core/config" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -96,7 +96,7 @@ func LoadClothoConfig(cfg *config.Config) (ClothoConfig, error) { // SaveAgent writes an agent config entry to the config file. func SaveAgent(cfg *config.Config, name string, ac AgentConfig) error { - key := fmt.Sprintf("agentci.agents.%s", name) + key := core.Sprintf("agentci.agents.%s", name) data := map[string]any{ "host": ac.Host, "queue_dir": ac.QueueDir, diff --git a/agentci/security.go b/agentci/security.go index 98d76a7..fb743b0 100644 --- a/agentci/security.go +++ b/agentci/security.go @@ -7,6 +7,7 @@ import ( "regexp" "strings" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -15,7 +16,7 @@ var safeNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]+$`) // SanitizePath ensures a filename or directory name is safe and prevents path traversal. // Returns filepath.Base of the input after validation. func SanitizePath(input string) (string, error) { - base := filepath.Base(input) + base := core.PathBase(input) safeBase, err := ValidatePathElement(base) if err != nil { return "", coreerr.E("agentci.SanitizePath", "invalid path element", err) @@ -28,13 +29,13 @@ func ValidatePathElement(input string) (string, error) { if input == "" { return "", coreerr.E("agentci.ValidatePathElement", "path element is empty", nil) } - if strings.TrimSpace(input) != input { + if core.Trim(input) != input { return "", coreerr.E("agentci.ValidatePathElement", "path element has leading or trailing whitespace", nil) } if input == "." || input == ".." { return "", coreerr.E("agentci.ValidatePathElement", "invalid path element: "+input, nil) } - if strings.ContainsAny(input, `/\`) || filepath.IsAbs(input) { + if strings.ContainsAny(input, `/\`) || core.PathIsAbs(input) { return "", coreerr.E("agentci.ValidatePathElement", "path element must not contain path separators", nil) } if !safeNameRegex.MatchString(input) { @@ -61,12 +62,12 @@ func ResolvePathWithinRoot(root, name string) (string, string, error) { } rootAbs = filepath.Clean(rootAbs) - resolved := filepath.Clean(filepath.Join(rootAbs, safeName)) + resolved := filepath.Clean(core.JoinPath(rootAbs, safeName)) rel, err := filepath.Rel(rootAbs, resolved) if err != nil { return "", "", coreerr.E("agentci.ResolvePathWithinRoot", "compute relative path", err) } - if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + if rel == ".." || core.HasPrefix(rel, ".."+string(filepath.Separator)) { return "", "", coreerr.E("agentci.ResolvePathWithinRoot", "resolved path escapes root", nil) } @@ -78,7 +79,7 @@ func ValidateRemoteDir(input string) (string, error) { if input == "" { return "", coreerr.E("agentci.ValidateRemoteDir", "remote dir is empty", nil) } - if strings.TrimSpace(input) != input { + if core.Trim(input) != input { return "", coreerr.E("agentci.ValidateRemoteDir", "remote dir has leading or trailing whitespace", nil) } if strings.ContainsAny(input, "\x00\r\n") { @@ -93,15 +94,15 @@ func ValidateRemoteDir(input string) (string, error) { rest string ) switch { - case strings.HasPrefix(input, "/"): - rest = strings.TrimPrefix(input, "/") - case strings.HasPrefix(input, "~/"): - rest = strings.TrimPrefix(input, "~/") + case core.HasPrefix(input, "/"): + rest = core.TrimPrefix(input, "/") + case core.HasPrefix(input, "~/"): + rest = core.TrimPrefix(input, "~/") default: return "", coreerr.E("agentci.ValidateRemoteDir", "remote dir must be absolute or home-relative", nil) } - for _, part := range strings.Split(rest, "/") { + for _, part := range core.Split(rest, "/") { if part == "" { continue } @@ -113,16 +114,16 @@ func ValidateRemoteDir(input string) (string, error) { } switch { - case strings.HasPrefix(input, "/"): + case core.HasPrefix(input, "/"): if len(parts) == 0 { return "/", nil } - return "/" + strings.Join(parts, "/"), nil + return "/" + core.Join("/", parts...), nil default: if len(parts) == 0 { return "~", nil } - return "~/" + strings.Join(parts, "/"), nil + return "~/" + core.Join("/", parts...), nil } } @@ -149,7 +150,7 @@ func JoinRemotePath(base string, elems ...string) (string, error) { // EscapeShellArg wraps a string in single quotes for safe remote shell insertion. // Prefer exec.Command arguments over constructing shell strings where possible. func EscapeShellArg(arg string) string { - return "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'" + return "'" + core.Replace(arg, "'", "'\\''") + "'" } // SecureSSHCommand creates an SSH exec.Cmd with strict host key checking and batch mode. diff --git a/agentci/security_test.go b/agentci/security_test.go index 408c8e7..63c96b9 100644 --- a/agentci/security_test.go +++ b/agentci/security_test.go @@ -3,7 +3,7 @@ package agentci import ( - "path/filepath" + core "dappco.re/go/core" "testing" "github.com/stretchr/testify/assert" @@ -106,7 +106,7 @@ func TestResolvePathWithinRoot_Good(t *testing.T) { gotName, gotPath, err := ResolvePathWithinRoot(root, "plugin-one") require.NoError(t, err) assert.Equal(t, "plugin-one", gotName) - assert.Equal(t, filepath.Join(root, "plugin-one"), gotPath) + assert.Equal(t, core.JoinPath(root, "plugin-one"), gotPath) } func TestResolvePathWithinRoot_Bad(t *testing.T) { diff --git a/cmd/collect/cmd.go b/cmd/collect/cmd.go index 8f7e43c..9b1c8e2 100644 --- a/cmd/collect/cmd.go +++ b/cmd/collect/cmd.go @@ -1,9 +1,9 @@ package collect import ( - "fmt" "forge.lthn.ai/core/cli/pkg/cli" + core "dappco.re/go/core" "dappco.re/go/core/scm/collect" "dappco.re/go/core/i18n" "dappco.re/go/core/io" @@ -90,21 +90,21 @@ func printResult(result *collect.Result) { } if result.Items > 0 { - cli.Success(fmt.Sprintf("Collected %d items from %s", result.Items, result.Source)) + cli.Success(core.Sprintf("Collected %d items from %s", result.Items, result.Source)) } else { - cli.Dim(fmt.Sprintf("No items collected from %s", result.Source)) + cli.Dim(core.Sprintf("No items collected from %s", result.Source)) } if result.Skipped > 0 { - cli.Dim(fmt.Sprintf(" Skipped: %d", result.Skipped)) + cli.Dim(core.Sprintf(" Skipped: %d", result.Skipped)) } if result.Errors > 0 { - cli.Warn(fmt.Sprintf(" Errors: %d", result.Errors)) + cli.Warn(core.Sprintf(" Errors: %d", result.Errors)) } if collectVerbose && len(result.Files) > 0 { - cli.Dim(fmt.Sprintf(" Files: %d", len(result.Files))) + cli.Dim(core.Sprintf(" Files: %d", len(result.Files))) for _, f := range result.Files { cli.Print(" %s\n", dimStyle.Render(f)) } diff --git a/cmd/collect/cmd_bitcointalk.go b/cmd/collect/cmd_bitcointalk.go index 0bab644..98f5d02 100644 --- a/cmd/collect/cmd_bitcointalk.go +++ b/cmd/collect/cmd_bitcointalk.go @@ -2,9 +2,9 @@ package collect import ( "context" - "strings" "forge.lthn.ai/core/cli/pkg/cli" + core "dappco.re/go/core" "dappco.re/go/core/scm/collect" "dappco.re/go/core/i18n" ) @@ -33,7 +33,7 @@ func runBitcoinTalk(target string) error { var topicID, url string // Determine if argument is a URL or topic ID - if strings.HasPrefix(target, "http") { + if core.HasPrefix(target, "http") { url = target } else { topicID = target diff --git a/cmd/collect/cmd_dispatch.go b/cmd/collect/cmd_dispatch.go index 9467e7f..b5d3469 100644 --- a/cmd/collect/cmd_dispatch.go +++ b/cmd/collect/cmd_dispatch.go @@ -1,11 +1,11 @@ package collect import ( - "fmt" "time" "forge.lthn.ai/core/cli/pkg/cli" collectpkg "dappco.re/go/core/scm/collect" + core "dappco.re/go/core" "dappco.re/go/core/i18n" ) @@ -53,12 +53,12 @@ func runDispatch(eventType string) error { event := collectpkg.Event{ Type: eventType, Source: "cli", - Message: fmt.Sprintf("Manual dispatch of %s event", eventType), + Message: core.Sprintf("Manual dispatch of %s event", eventType), Time: time.Now(), } cfg.Dispatcher.Emit(event) - cli.Success(fmt.Sprintf("Dispatched %s event", eventType)) + cli.Success(core.Sprintf("Dispatched %s event", eventType)) return nil } @@ -125,6 +125,6 @@ func runHooksRegister(eventType, command string) error { return cli.Err("unknown event type: %s (valid: start, progress, item, error, complete)", eventType) } - cli.Success(fmt.Sprintf("Registered hook for %s: %s", eventType, command)) + cli.Success(core.Sprintf("Registered hook for %s: %s", eventType, command)) return nil } diff --git a/cmd/collect/cmd_excavate.go b/cmd/collect/cmd_excavate.go index a9bf038..81ca895 100644 --- a/cmd/collect/cmd_excavate.go +++ b/cmd/collect/cmd_excavate.go @@ -2,9 +2,9 @@ package collect import ( "context" - "fmt" "forge.lthn.ai/core/cli/pkg/cli" + core "dappco.re/go/core" "dappco.re/go/core/scm/collect" "dappco.re/go/core/i18n" ) @@ -57,9 +57,9 @@ func runExcavate(project string) error { } if cfg.DryRun { - cli.Info(fmt.Sprintf("Dry run: would excavate project %s with %d collectors", project, len(collectors))) + cli.Info(core.Sprintf("Dry run: would excavate project %s with %d collectors", project, len(collectors))) for _, c := range collectors { - cli.Dim(fmt.Sprintf(" - %s", c.Name())) + cli.Dim(core.Sprintf(" - %s", c.Name())) } return nil } diff --git a/cmd/collect/cmd_github.go b/cmd/collect/cmd_github.go index 22c1f7d..509d11f 100644 --- a/cmd/collect/cmd_github.go +++ b/cmd/collect/cmd_github.go @@ -2,9 +2,9 @@ package collect import ( "context" - "strings" "forge.lthn.ai/core/cli/pkg/cli" + core "dappco.re/go/core" "dappco.re/go/core/scm/collect" "dappco.re/go/core/i18n" ) @@ -42,8 +42,8 @@ func runGitHub(target string) error { // Parse org/repo argument var org, repo string - if strings.Contains(target, "/") { - parts := strings.SplitN(target, "/", 2) + if core.Contains(target, "/") { + parts := core.SplitN(target, "/", 2) org = parts[0] repo = parts[1] } else if githubOrg { diff --git a/cmd/forge/cmd_auth.go b/cmd/forge/cmd_auth.go index a707bf5..290be31 100644 --- a/cmd/forge/cmd_auth.go +++ b/cmd/forge/cmd_auth.go @@ -1,8 +1,8 @@ package forge import ( - "fmt" + core "dappco.re/go/core" "forge.lthn.ai/core/cli/pkg/cli" fg "dappco.re/go/core/scm/forge" ) @@ -37,7 +37,7 @@ func runAuth() error { return err } if authURL != "" { - cli.Success(fmt.Sprintf("URL set to %s", authURL)) + cli.Success(core.Sprintf("URL set to %s", authURL)) } if authToken != "" { cli.Success("Token saved") @@ -54,7 +54,7 @@ func runAuth() error { cli.Blank() cli.Print(" %s %s\n", dimStyle.Render("URL:"), valueStyle.Render(url)) cli.Print(" %s %s\n", dimStyle.Render("Auth:"), warningStyle.Render("not authenticated")) - cli.Print(" %s %s\n", dimStyle.Render("Hint:"), dimStyle.Render(fmt.Sprintf("core forge auth --token TOKEN (create at %s/user/settings/applications)", url))) + cli.Print(" %s %s\n", dimStyle.Render("Hint:"), dimStyle.Render(core.Sprintf("core forge auth --token TOKEN (create at %s/user/settings/applications)", url))) cli.Blank() return nil } @@ -74,7 +74,7 @@ func runAuth() error { } cli.Blank() - cli.Success(fmt.Sprintf("Authenticated to %s", client.URL())) + cli.Success(core.Sprintf("Authenticated to %s", client.URL())) cli.Print(" %s %s\n", dimStyle.Render("User:"), valueStyle.Render(user.UserName)) cli.Print(" %s %s\n", dimStyle.Render("Email:"), valueStyle.Render(user.Email)) if user.IsAdmin { diff --git a/cmd/forge/cmd_config.go b/cmd/forge/cmd_config.go index 94c5679..eb11092 100644 --- a/cmd/forge/cmd_config.go +++ b/cmd/forge/cmd_config.go @@ -1,8 +1,8 @@ package forge import ( - "fmt" + core "dappco.re/go/core" "forge.lthn.ai/core/cli/pkg/cli" fg "dappco.re/go/core/scm/forge" ) @@ -40,7 +40,7 @@ func runConfig() error { } if configURL != "" { - cli.Success(fmt.Sprintf("Forgejo URL set to %s", configURL)) + cli.Success(core.Sprintf("Forgejo URL set to %s", configURL)) } if configToken != "" { cli.Success("Forgejo token saved") @@ -97,7 +97,7 @@ func runConfigTest() error { } cli.Blank() - cli.Success(fmt.Sprintf("Connected to %s", client.URL())) + cli.Success(core.Sprintf("Connected to %s", client.URL())) cli.Print(" %s %s\n", dimStyle.Render("User:"), valueStyle.Render(user.UserName)) cli.Print(" %s %s\n", dimStyle.Render("Email:"), valueStyle.Render(user.Email)) cli.Blank() diff --git a/cmd/forge/cmd_issues.go b/cmd/forge/cmd_issues.go index 108d237..de17a25 100644 --- a/cmd/forge/cmd_issues.go +++ b/cmd/forge/cmd_issues.go @@ -1,8 +1,8 @@ package forge import ( - "fmt" - "strings" + + core "dappco.re/go/core" forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2" @@ -104,7 +104,7 @@ func runListAllIssues() error { continue } - cli.Print(" %s %s\n", repoStyle.Render(repo.FullName), dimStyle.Render(fmt.Sprintf("(%d)", len(issues)))) + cli.Print(" %s %s\n", repoStyle.Render(repo.FullName), dimStyle.Render(core.Sprintf("(%d)", len(issues)))) for _, issue := range issues { printForgeIssue(issue) } @@ -113,9 +113,9 @@ func runListAllIssues() error { } if total == 0 { - cli.Text(fmt.Sprintf("No %s issues found.", issuesState)) + cli.Text(core.Sprintf("No %s issues found.", issuesState)) } else { - cli.Print(" %s\n", dimStyle.Render(fmt.Sprintf("%d %s issues total", total, issuesState))) + cli.Print(" %s\n", dimStyle.Render(core.Sprintf("%d %s issues total", total, issuesState))) } cli.Blank() @@ -136,12 +136,12 @@ func runListIssues(owner, repo string) error { } if len(issues) == 0 { - cli.Text(fmt.Sprintf("No %s issues in %s/%s.", issuesState, owner, repo)) + cli.Text(core.Sprintf("No %s issues in %s/%s.", issuesState, owner, repo)) return nil } cli.Blank() - cli.Print(" %s\n\n", fmt.Sprintf("%d %s issues in %s/%s", len(issues), issuesState, owner, repo)) + cli.Print(" %s\n\n", core.Sprintf("%d %s issues in %s/%s", len(issues), issuesState, owner, repo)) for _, issue := range issues { printForgeIssue(issue) @@ -165,7 +165,7 @@ func runCreateIssue(owner, repo string) error { } cli.Blank() - cli.Success(fmt.Sprintf("Created issue #%d: %s", issue.Index, issue.Title)) + cli.Success(core.Sprintf("Created issue #%d: %s", issue.Index, issue.Title)) cli.Print(" %s %s\n", dimStyle.Render("URL:"), valueStyle.Render(issue.HTMLURL)) cli.Blank() @@ -173,10 +173,10 @@ func runCreateIssue(owner, repo string) error { } func printForgeIssue(issue *forgejo.Issue) { - num := numberStyle.Render(fmt.Sprintf("#%d", issue.Index)) + num := numberStyle.Render(core.Sprintf("#%d", issue.Index)) title := valueStyle.Render(cli.Truncate(issue.Title, 60)) - line := fmt.Sprintf(" %s %s", num, title) + line := core.Sprintf(" %s %s", num, title) // Add labels if len(issue.Labels) > 0 { @@ -184,7 +184,7 @@ func printForgeIssue(issue *forgejo.Issue) { for _, l := range issue.Labels { labels = append(labels, l.Name) } - line += " " + warningStyle.Render("["+strings.Join(labels, ", ")+"]") + line += " " + warningStyle.Render("["+core.Join(", ", labels...)+"]") } // Add assignees @@ -193,7 +193,7 @@ func printForgeIssue(issue *forgejo.Issue) { for _, a := range issue.Assignees { assignees = append(assignees, "@"+a.UserName) } - line += " " + infoStyle.Render(strings.Join(assignees, ", ")) + line += " " + infoStyle.Render(core.Join(", ", assignees...)) } cli.Text(line) diff --git a/cmd/forge/cmd_labels.go b/cmd/forge/cmd_labels.go index 745ab61..e3129c9 100644 --- a/cmd/forge/cmd_labels.go +++ b/cmd/forge/cmd_labels.go @@ -1,7 +1,8 @@ package forge import ( - "fmt" + + core "dappco.re/go/core" forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2" @@ -67,7 +68,7 @@ func runListLabels(org string) error { } cli.Blank() - cli.Print(" %s\n\n", fmt.Sprintf("%d labels", len(labels))) + cli.Print(" %s\n\n", core.Sprintf("%d labels", len(labels))) table := cli.NewTable("Name", "Color", "Description") @@ -113,7 +114,7 @@ func runCreateLabel(org string) error { } cli.Blank() - cli.Success(fmt.Sprintf("Created label %q on %s/%s", label.Name, org, repo)) + cli.Success(core.Sprintf("Created label %q on %s/%s", label.Name, org, repo)) cli.Blank() return nil diff --git a/cmd/forge/cmd_migrate.go b/cmd/forge/cmd_migrate.go index 8f8a8bb..df53a20 100644 --- a/cmd/forge/cmd_migrate.go +++ b/cmd/forge/cmd_migrate.go @@ -1,7 +1,8 @@ package forge import ( - "fmt" + + core "dappco.re/go/core" forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2" @@ -92,7 +93,7 @@ func runMigrate(cloneURL string) error { } cli.Blank() - cli.Success(fmt.Sprintf("Migration complete: %s", repo.FullName)) + cli.Success(core.Sprintf("Migration complete: %s", repo.FullName)) cli.Print(" %s %s\n", dimStyle.Render("URL:"), valueStyle.Render(repo.HTMLURL)) cli.Print(" %s %s\n", dimStyle.Render("Clone:"), valueStyle.Render(repo.CloneURL)) if migrateMirror { diff --git a/cmd/forge/cmd_orgs.go b/cmd/forge/cmd_orgs.go index 27389a3..07ad1a3 100644 --- a/cmd/forge/cmd_orgs.go +++ b/cmd/forge/cmd_orgs.go @@ -1,8 +1,8 @@ package forge import ( - "fmt" + core "dappco.re/go/core" "forge.lthn.ai/core/cli/pkg/cli" fg "dappco.re/go/core/scm/forge" ) @@ -38,7 +38,7 @@ func runOrgs() error { } cli.Blank() - cli.Print(" %s\n\n", fmt.Sprintf("%d organisations", len(orgs))) + cli.Print(" %s\n\n", core.Sprintf("%d organisations", len(orgs))) table := cli.NewTable("Name", "Visibility", "Description") diff --git a/cmd/forge/cmd_prs.go b/cmd/forge/cmd_prs.go index e32db77..a32b288 100644 --- a/cmd/forge/cmd_prs.go +++ b/cmd/forge/cmd_prs.go @@ -1,8 +1,8 @@ package forge import ( - "fmt" - "strings" + + core "dappco.re/go/core" forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2" @@ -48,12 +48,12 @@ func runListPRs(owner, repo string) error { } if len(prs) == 0 { - cli.Text(fmt.Sprintf("No %s pull requests in %s/%s.", prsState, owner, repo)) + cli.Text(core.Sprintf("No %s pull requests in %s/%s.", prsState, owner, repo)) return nil } cli.Blank() - cli.Print(" %s\n\n", fmt.Sprintf("%d %s pull requests in %s/%s", len(prs), prsState, owner, repo)) + cli.Print(" %s\n\n", core.Sprintf("%d %s pull requests in %s/%s", len(prs), prsState, owner, repo)) for _, pr := range prs { printForgePR(pr) @@ -63,7 +63,7 @@ func runListPRs(owner, repo string) error { } func printForgePR(pr *forgejo.PullRequest) { - num := numberStyle.Render(fmt.Sprintf("#%d", pr.Index)) + num := numberStyle.Render(core.Sprintf("#%d", pr.Index)) title := valueStyle.Render(cli.Truncate(pr.Title, 50)) var author string @@ -91,7 +91,7 @@ func printForgePR(pr *forgejo.PullRequest) { for _, l := range pr.Labels { labels = append(labels, l.Name) } - labelStr = " " + warningStyle.Render("["+strings.Join(labels, ", ")+"]") + labelStr = " " + warningStyle.Render("["+core.Join(", ", labels...)+"]") } cli.Print(" %s %s %s %s %s%s\n", num, title, author, status, branch, labelStr) diff --git a/cmd/forge/cmd_repos.go b/cmd/forge/cmd_repos.go index 71cb2a9..aa57d92 100644 --- a/cmd/forge/cmd_repos.go +++ b/cmd/forge/cmd_repos.go @@ -1,7 +1,8 @@ package forge import ( - "fmt" + + core "dappco.re/go/core" forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2" @@ -82,12 +83,12 @@ func runRepos() error { repoStyle.Render(r.FullName), dimStyle.Render(repoType), visibility, - fmt.Sprintf("%d", r.Stars), + core.Sprintf("%d", r.Stars), ) } cli.Blank() - cli.Print(" %s\n\n", fmt.Sprintf("%d repositories", len(repos))) + cli.Print(" %s\n\n", core.Sprintf("%d repositories", len(repos))) table.Render() return nil diff --git a/cmd/forge/cmd_status.go b/cmd/forge/cmd_status.go index 4777894..8b256eb 100644 --- a/cmd/forge/cmd_status.go +++ b/cmd/forge/cmd_status.go @@ -1,8 +1,8 @@ package forge import ( - "fmt" + core "dappco.re/go/core" "forge.lthn.ai/core/cli/pkg/cli" fg "dappco.re/go/core/scm/forge" ) @@ -55,8 +55,8 @@ func runStatus() error { cli.Print(" %s %s\n", dimStyle.Render("Instance:"), valueStyle.Render(client.URL())) cli.Print(" %s %s\n", dimStyle.Render("Version:"), valueStyle.Render(ver)) cli.Print(" %s %s\n", dimStyle.Render("User:"), valueStyle.Render(user.UserName)) - cli.Print(" %s %s\n", dimStyle.Render("Orgs:"), numberStyle.Render(fmt.Sprintf("%d", len(orgs)))) - cli.Print(" %s %s\n", dimStyle.Render("Repos:"), numberStyle.Render(fmt.Sprintf("%d", len(repos)))) + cli.Print(" %s %s\n", dimStyle.Render("Orgs:"), numberStyle.Render(core.Sprintf("%d", len(orgs)))) + cli.Print(" %s %s\n", dimStyle.Render("Repos:"), numberStyle.Render(core.Sprintf("%d", len(repos)))) cli.Blank() return nil diff --git a/cmd/forge/cmd_sync.go b/cmd/forge/cmd_sync.go index 7a4176c..30757e8 100644 --- a/cmd/forge/cmd_sync.go +++ b/cmd/forge/cmd_sync.go @@ -1,13 +1,11 @@ package forge import ( - "fmt" "net/url" "os" "os/exec" - "path/filepath" - "strings" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "dappco.re/go/core/scm/agentci" fg "dappco.re/go/core/scm/forge" @@ -64,12 +62,12 @@ func runSync(args []string) error { // Expand base path basePath := syncBasePath - if strings.HasPrefix(basePath, "~/") { + if core.HasPrefix(basePath, "~/") { home, err := os.UserHomeDir() if err != nil { return coreerr.E("forge.runSync", "failed to resolve home directory", err) } - basePath = filepath.Join(home, basePath[2:]) + basePath = core.JoinPath(home, basePath[2:]) } // Build repo list: either from args or from the Forgejo org @@ -172,7 +170,7 @@ func runSyncSetup(client *fg.Client, repos []syncRepoEntry, forgeURL string) err // Step 3: Add forge remote to local clone cli.Print(" Configuring remote... ") - remoteURL := fmt.Sprintf("%s/%s/%s.git", forgeURL, syncOrg, repo.name) + remoteURL := core.Sprintf("%s/%s/%s.git", forgeURL, syncOrg, repo.name) err = syncConfigureForgeRemote(repo.localPath, remoteURL) if err != nil { cli.Print("%s\n", errorStyle.Render(err.Error())) @@ -195,7 +193,7 @@ func runSyncSetup(client *fg.Client, repos []syncRepoEntry, forgeURL string) err cli.Print(" Creating main branch... ") err = syncCreateMainFromUpstream(client, syncOrg, repo.name) if err != nil { - if strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "409") { + if core.Contains(err.Error(), "already exists") || core.Contains(err.Error(), "409") { cli.Print("%s\n", dimStyle.Render("exists")) } else { cli.Print("%s\n", errorStyle.Render(err.Error())) @@ -221,9 +219,9 @@ func runSyncSetup(client *fg.Client, repos []syncRepoEntry, forgeURL string) err cli.Blank() } - cli.Print(" %s", successStyle.Render(fmt.Sprintf("%d repos set up", succeeded))) + cli.Print(" %s", successStyle.Render(core.Sprintf("%d repos set up", succeeded))) if failed > 0 { - cli.Print(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed))) + cli.Print(", %s", errorStyle.Render(core.Sprintf("%d failed", failed))) } cli.Blank() @@ -240,7 +238,7 @@ func runSyncUpdate(repos []syncRepoEntry, forgeURL string) error { cli.Print(" %s -> upstream ", repoStyle.Render(repo.name)) // Ensure remote exists - remoteURL := fmt.Sprintf("%s/%s/%s.git", forgeURL, syncOrg, repo.name) + remoteURL := core.Sprintf("%s/%s/%s.git", forgeURL, syncOrg, repo.name) _ = syncConfigureForgeRemote(repo.localPath, remoteURL) // Fetch latest from GitHub (origin) @@ -264,9 +262,9 @@ func runSyncUpdate(repos []syncRepoEntry, forgeURL string) error { } cli.Blank() - cli.Print(" %s", successStyle.Render(fmt.Sprintf("%d synced", succeeded))) + cli.Print(" %s", successStyle.Render(core.Sprintf("%d synced", succeeded))) if failed > 0 { - cli.Print(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed))) + cli.Print(", %s", errorStyle.Render(core.Sprintf("%d failed", failed))) } cli.Blank() @@ -276,15 +274,15 @@ func runSyncUpdate(repos []syncRepoEntry, forgeURL string) error { func syncDetectDefaultBranch(path string) string { out, err := exec.Command("git", "-C", path, "symbolic-ref", "refs/remotes/origin/HEAD").Output() if err == nil { - ref := strings.TrimSpace(string(out)) - if parts := strings.Split(ref, "/"); len(parts) > 0 { + ref := core.Trim(string(out)) + if parts := core.Split(ref, "/"); len(parts) > 0 { return parts[len(parts)-1] } } out, err = exec.Command("git", "-C", path, "branch", "--show-current").Output() if err == nil { - branch := strings.TrimSpace(string(out)) + branch := core.Trim(string(out)) if branch != "" { return branch } @@ -296,7 +294,7 @@ func syncDetectDefaultBranch(path string) string { func syncConfigureForgeRemote(localPath, remoteURL string) error { out, err := exec.Command("git", "-C", localPath, "remote", "get-url", "forge").Output() if err == nil { - existing := strings.TrimSpace(string(out)) + existing := core.Trim(string(out)) if existing != remoteURL { cmd := exec.Command("git", "-C", localPath, "remote", "set-url", "forge", remoteURL) if err := cmd.Run(); err != nil { @@ -315,11 +313,11 @@ func syncConfigureForgeRemote(localPath, remoteURL string) error { } func syncPushUpstream(localPath, defaultBranch string) error { - refspec := fmt.Sprintf("refs/remotes/origin/%s:refs/heads/upstream", defaultBranch) + refspec := core.Sprintf("refs/remotes/origin/%s:refs/heads/upstream", defaultBranch) cmd := exec.Command("git", "-C", localPath, "push", "--force", "forge", refspec) output, err := cmd.CombinedOutput() if err != nil { - return coreerr.E("forge.syncPushUpstream", strings.TrimSpace(string(output)), err) + return coreerr.E("forge.syncPushUpstream", core.Trim(string(output)), err) } return nil @@ -329,7 +327,7 @@ func syncGitFetch(localPath, remote string) error { cmd := exec.Command("git", "-C", localPath, "fetch", remote) output, err := cmd.CombinedOutput() if err != nil { - return coreerr.E("forge.syncGitFetch", strings.TrimSpace(string(output)), err) + return coreerr.E("forge.syncGitFetch", core.Trim(string(output)), err) } return nil } @@ -352,7 +350,7 @@ func syncRepoNameFromArg(arg string) (string, error) { return "", coreerr.E("forge.syncRepoNameFromArg", "decode repo argument", err) } - parts := strings.Split(decoded, "/") + parts := core.Split(decoded, "/") switch len(parts) { case 1: return agentci.ValidatePathElement(parts[0]) diff --git a/cmd/forge/cmd_sync_test.go b/cmd/forge/cmd_sync_test.go index c75d74b..e1c1fce 100644 --- a/cmd/forge/cmd_sync_test.go +++ b/cmd/forge/cmd_sync_test.go @@ -1,7 +1,7 @@ package forge import ( - "path/filepath" + core "dappco.re/go/core" "testing" "github.com/stretchr/testify/assert" @@ -9,17 +9,17 @@ import ( ) func TestBuildSyncRepoList_Good(t *testing.T) { - basePath := filepath.Join(t.TempDir(), "repos") + basePath := core.JoinPath(t.TempDir(), "repos") repos, err := buildSyncRepoList(nil, []string{"host-uk/core"}, basePath) require.NoError(t, err) require.Len(t, repos, 1) assert.Equal(t, "core", repos[0].name) - assert.Equal(t, filepath.Join(basePath, "core"), repos[0].localPath) + assert.Equal(t, core.JoinPath(basePath, "core"), repos[0].localPath) } func TestBuildSyncRepoList_Bad_PathTraversal(t *testing.T) { - basePath := filepath.Join(t.TempDir(), "repos") + basePath := core.JoinPath(t.TempDir(), "repos") _, err := buildSyncRepoList(nil, []string{"../escape"}, basePath) require.Error(t, err) @@ -27,17 +27,17 @@ func TestBuildSyncRepoList_Bad_PathTraversal(t *testing.T) { } func TestBuildSyncRepoList_Good_OwnerRepo(t *testing.T) { - basePath := filepath.Join(t.TempDir(), "repos") + basePath := core.JoinPath(t.TempDir(), "repos") repos, err := buildSyncRepoList(nil, []string{"Host-UK/core"}, basePath) require.NoError(t, err) require.Len(t, repos, 1) assert.Equal(t, "core", repos[0].name) - assert.Equal(t, filepath.Join(basePath, "core"), repos[0].localPath) + assert.Equal(t, core.JoinPath(basePath, "core"), repos[0].localPath) } func TestBuildSyncRepoList_Bad_PathTraversal_OwnerRepo(t *testing.T) { - basePath := filepath.Join(t.TempDir(), "repos") + basePath := core.JoinPath(t.TempDir(), "repos") _, err := buildSyncRepoList(nil, []string{"host-uk/../escape"}, basePath) require.Error(t, err) @@ -45,7 +45,7 @@ func TestBuildSyncRepoList_Bad_PathTraversal_OwnerRepo(t *testing.T) { } func TestBuildSyncRepoList_Bad_PathTraversal_OwnerRepoEncoded(t *testing.T) { - basePath := filepath.Join(t.TempDir(), "repos") + basePath := core.JoinPath(t.TempDir(), "repos") _, err := buildSyncRepoList(nil, []string{"host-uk%2F..%2Fescape"}, basePath) require.Error(t, err) diff --git a/cmd/forge/helpers.go b/cmd/forge/helpers.go index eec2d68..f542b36 100644 --- a/cmd/forge/helpers.go +++ b/cmd/forge/helpers.go @@ -2,14 +2,14 @@ package forge import ( "path" - "strings" + core "dappco.re/go/core" "forge.lthn.ai/core/cli/pkg/cli" ) // splitOwnerRepo splits "owner/repo" into its parts. func splitOwnerRepo(s string) (string, string, error) { - parts := strings.SplitN(s, "/", 2) + parts := core.SplitN(s, "/", 2) if len(parts) != 2 || parts[0] == "" || parts[1] == "" { return "", "", cli.Err("expected format: owner/repo (got %q)", s) } @@ -25,7 +25,7 @@ func extractRepoName(cloneURL string) string { // Get the last path segment name := path.Base(cloneURL) // Strip .git suffix - name = strings.TrimSuffix(name, ".git") + name = core.TrimSuffix(name, ".git") if name == "" || name == "." || name == "/" { return "" } diff --git a/cmd/gitea/cmd_config.go b/cmd/gitea/cmd_config.go index 69f07f3..e4197ad 100644 --- a/cmd/gitea/cmd_config.go +++ b/cmd/gitea/cmd_config.go @@ -1,8 +1,8 @@ package gitea import ( - "fmt" + core "dappco.re/go/core" "forge.lthn.ai/core/cli/pkg/cli" gt "dappco.re/go/core/scm/gitea" ) @@ -40,7 +40,7 @@ func runConfig() error { } if configURL != "" { - cli.Success(fmt.Sprintf("Gitea URL set to %s", configURL)) + cli.Success(core.Sprintf("Gitea URL set to %s", configURL)) } if configToken != "" { cli.Success("Gitea token saved") @@ -97,7 +97,7 @@ func runConfigTest() error { } cli.Blank() - cli.Success(fmt.Sprintf("Connected to %s", client.URL())) + cli.Success(core.Sprintf("Connected to %s", client.URL())) cli.Print(" %s %s\n", dimStyle.Render("User:"), valueStyle.Render(user.UserName)) cli.Print(" %s %s\n", dimStyle.Render("Email:"), valueStyle.Render(user.Email)) cli.Blank() diff --git a/cmd/gitea/cmd_issues.go b/cmd/gitea/cmd_issues.go index a4fd5e2..c6bcc95 100644 --- a/cmd/gitea/cmd_issues.go +++ b/cmd/gitea/cmd_issues.go @@ -1,11 +1,10 @@ package gitea import ( - "fmt" - "strings" "code.gitea.io/sdk/gitea" + core "dappco.re/go/core" "forge.lthn.ai/core/cli/pkg/cli" gt "dappco.re/go/core/scm/gitea" ) @@ -60,12 +59,12 @@ func runListIssues(owner, repo string) error { } if len(issues) == 0 { - cli.Text(fmt.Sprintf("No %s issues in %s/%s.", issuesState, owner, repo)) + cli.Text(core.Sprintf("No %s issues in %s/%s.", issuesState, owner, repo)) return nil } cli.Blank() - cli.Print(" %s\n\n", fmt.Sprintf("%d %s issues in %s/%s", len(issues), issuesState, owner, repo)) + cli.Print(" %s\n\n", core.Sprintf("%d %s issues in %s/%s", len(issues), issuesState, owner, repo)) for _, issue := range issues { printGiteaIssue(issue, owner, repo) @@ -89,7 +88,7 @@ func runCreateIssue(owner, repo string) error { } cli.Blank() - cli.Success(fmt.Sprintf("Created issue #%d: %s", issue.Index, issue.Title)) + cli.Success(core.Sprintf("Created issue #%d: %s", issue.Index, issue.Title)) cli.Print(" %s %s\n", dimStyle.Render("URL:"), valueStyle.Render(issue.HTMLURL)) cli.Blank() @@ -97,10 +96,10 @@ func runCreateIssue(owner, repo string) error { } func printGiteaIssue(issue *gitea.Issue, owner, repo string) { - num := numberStyle.Render(fmt.Sprintf("#%d", issue.Index)) + num := numberStyle.Render(core.Sprintf("#%d", issue.Index)) title := valueStyle.Render(cli.Truncate(issue.Title, 60)) - line := fmt.Sprintf(" %s %s", num, title) + line := core.Sprintf(" %s %s", num, title) // Add labels if len(issue.Labels) > 0 { @@ -108,7 +107,7 @@ func printGiteaIssue(issue *gitea.Issue, owner, repo string) { for _, l := range issue.Labels { labels = append(labels, l.Name) } - line += " " + warningStyle.Render("["+strings.Join(labels, ", ")+"]") + line += " " + warningStyle.Render("["+core.Join(", ", labels...)+"]") } // Add assignees @@ -117,7 +116,7 @@ func printGiteaIssue(issue *gitea.Issue, owner, repo string) { for _, a := range issue.Assignees { assignees = append(assignees, "@"+a.UserName) } - line += " " + infoStyle.Render(strings.Join(assignees, ", ")) + line += " " + infoStyle.Render(core.Join(", ", assignees...)) } cli.Text(line) @@ -125,7 +124,7 @@ func printGiteaIssue(issue *gitea.Issue, owner, repo string) { // splitOwnerRepo splits "owner/repo" into its parts. func splitOwnerRepo(s string) (string, string, error) { - parts := strings.SplitN(s, "/", 2) + parts := core.SplitN(s, "/", 2) if len(parts) != 2 || parts[0] == "" || parts[1] == "" { return "", "", cli.Err("expected format: owner/repo (got %q)", s) } diff --git a/cmd/gitea/cmd_mirror.go b/cmd/gitea/cmd_mirror.go index e108686..f68aed9 100644 --- a/cmd/gitea/cmd_mirror.go +++ b/cmd/gitea/cmd_mirror.go @@ -1,10 +1,9 @@ package gitea import ( - "fmt" "os/exec" - "strings" + core "dappco.re/go/core" "forge.lthn.ai/core/cli/pkg/cli" gt "dappco.re/go/core/scm/gitea" ) @@ -48,7 +47,7 @@ func runMirror(githubOwner, githubRepo string) error { return err } - cloneURL := fmt.Sprintf("https://github.com/%s/%s.git", githubOwner, githubRepo) + cloneURL := core.Sprintf("https://github.com/%s/%s.git", githubOwner, githubRepo) // Determine target owner on Gitea targetOwner := mirrorOrg @@ -74,7 +73,7 @@ func runMirror(githubOwner, githubRepo string) error { } cli.Blank() - cli.Success(fmt.Sprintf("Mirror created: %s", repo.FullName)) + cli.Success(core.Sprintf("Mirror created: %s", repo.FullName)) cli.Print(" %s %s\n", dimStyle.Render("URL:"), valueStyle.Render(repo.HTMLURL)) cli.Print(" %s %s\n", dimStyle.Render("Clone:"), valueStyle.Render(repo.CloneURL)) cli.Blank() @@ -88,5 +87,5 @@ func resolveGHToken() string { if err != nil { return "" } - return strings.TrimSpace(string(out)) + return core.Trim(string(out)) } diff --git a/cmd/gitea/cmd_prs.go b/cmd/gitea/cmd_prs.go index 00e059e..8e967fa 100644 --- a/cmd/gitea/cmd_prs.go +++ b/cmd/gitea/cmd_prs.go @@ -1,11 +1,10 @@ package gitea import ( - "fmt" - "strings" sdk "code.gitea.io/sdk/gitea" + core "dappco.re/go/core" "forge.lthn.ai/core/cli/pkg/cli" gt "dappco.re/go/core/scm/gitea" ) @@ -48,12 +47,12 @@ func runListPRs(owner, repo string) error { } if len(prs) == 0 { - cli.Text(fmt.Sprintf("No %s pull requests in %s/%s.", prsState, owner, repo)) + cli.Text(core.Sprintf("No %s pull requests in %s/%s.", prsState, owner, repo)) return nil } cli.Blank() - cli.Print(" %s\n\n", fmt.Sprintf("%d %s pull requests in %s/%s", len(prs), prsState, owner, repo)) + cli.Print(" %s\n\n", core.Sprintf("%d %s pull requests in %s/%s", len(prs), prsState, owner, repo)) for _, pr := range prs { printGiteaPR(pr) @@ -63,7 +62,7 @@ func runListPRs(owner, repo string) error { } func printGiteaPR(pr *sdk.PullRequest) { - num := numberStyle.Render(fmt.Sprintf("#%d", pr.Index)) + num := numberStyle.Render(core.Sprintf("#%d", pr.Index)) title := valueStyle.Render(cli.Truncate(pr.Title, 50)) var author string @@ -91,7 +90,7 @@ func printGiteaPR(pr *sdk.PullRequest) { for _, l := range pr.Labels { labels = append(labels, l.Name) } - labelStr = " " + warningStyle.Render("["+strings.Join(labels, ", ")+"]") + labelStr = " " + warningStyle.Render("["+core.Join(", ", labels...)+"]") } cli.Print(" %s %s %s %s %s%s\n", num, title, author, status, branch, labelStr) diff --git a/cmd/gitea/cmd_repos.go b/cmd/gitea/cmd_repos.go index 892bdfe..0f4561e 100644 --- a/cmd/gitea/cmd_repos.go +++ b/cmd/gitea/cmd_repos.go @@ -1,8 +1,8 @@ package gitea import ( - "fmt" + core "dappco.re/go/core" "forge.lthn.ai/core/cli/pkg/cli" gt "dappco.re/go/core/scm/gitea" ) @@ -103,12 +103,12 @@ func runRepos() error { repoStyle.Render(r.FullName), dimStyle.Render(repoType), visibility, - fmt.Sprintf("%d", r.Stars), + core.Sprintf("%d", r.Stars), ) } cli.Blank() - cli.Print(" %s\n\n", fmt.Sprintf("%d repositories", len(repos))) + cli.Print(" %s\n\n", core.Sprintf("%d repositories", len(repos))) table.Render() return nil diff --git a/cmd/gitea/cmd_sync.go b/cmd/gitea/cmd_sync.go index de14da3..a52369f 100644 --- a/cmd/gitea/cmd_sync.go +++ b/cmd/gitea/cmd_sync.go @@ -1,13 +1,11 @@ package gitea import ( - "fmt" "net/url" "os" "os/exec" - "path/filepath" - "strings" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "dappco.re/go/core/scm/agentci" gt "dappco.re/go/core/scm/gitea" @@ -64,12 +62,12 @@ func runSync(args []string) error { // Expand base path basePath := syncBasePath - if strings.HasPrefix(basePath, "~/") { + if core.HasPrefix(basePath, "~/") { home, err := os.UserHomeDir() if err != nil { return coreerr.E("gitea.runSync", "failed to resolve home directory", err) } - basePath = filepath.Join(home, basePath[2:]) + basePath = core.JoinPath(home, basePath[2:]) } // Build repo list: either from args or from the Gitea org @@ -175,7 +173,7 @@ func runSyncSetup(client *gt.Client, repos []repoEntry, giteaURL string) error { // Step 3: Add gitea remote to local clone cli.Print(" Configuring remote... ") - remoteURL := fmt.Sprintf("%s/%s/%s.git", giteaURL, syncOrg, repo.name) + remoteURL := core.Sprintf("%s/%s/%s.git", giteaURL, syncOrg, repo.name) err = configureGiteaRemote(repo.localPath, remoteURL) if err != nil { cli.Print("%s\n", errorStyle.Render(err.Error())) @@ -198,7 +196,7 @@ func runSyncSetup(client *gt.Client, repos []repoEntry, giteaURL string) error { cli.Print(" Creating main branch... ") err = createMainFromUpstream(client, syncOrg, repo.name) if err != nil { - if strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "409") { + if core.Contains(err.Error(), "already exists") || core.Contains(err.Error(), "409") { cli.Print("%s\n", dimStyle.Render("exists")) } else { cli.Print("%s\n", errorStyle.Render(err.Error())) @@ -224,9 +222,9 @@ func runSyncSetup(client *gt.Client, repos []repoEntry, giteaURL string) error { cli.Blank() } - cli.Print(" %s", successStyle.Render(fmt.Sprintf("%d repos set up", succeeded))) + cli.Print(" %s", successStyle.Render(core.Sprintf("%d repos set up", succeeded))) if failed > 0 { - cli.Print(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed))) + cli.Print(", %s", errorStyle.Render(core.Sprintf("%d failed", failed))) } cli.Blank() @@ -244,7 +242,7 @@ func runSyncUpdate(repos []repoEntry, giteaURL string) error { cli.Print(" %s -> upstream ", repoStyle.Render(repo.name)) // Ensure remote exists - remoteURL := fmt.Sprintf("%s/%s/%s.git", giteaURL, syncOrg, repo.name) + remoteURL := core.Sprintf("%s/%s/%s.git", giteaURL, syncOrg, repo.name) _ = configureGiteaRemote(repo.localPath, remoteURL) // Fetch latest from GitHub (origin) @@ -268,9 +266,9 @@ func runSyncUpdate(repos []repoEntry, giteaURL string) error { } cli.Blank() - cli.Print(" %s", successStyle.Render(fmt.Sprintf("%d synced", succeeded))) + cli.Print(" %s", successStyle.Render(core.Sprintf("%d synced", succeeded))) if failed > 0 { - cli.Print(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed))) + cli.Print(", %s", errorStyle.Render(core.Sprintf("%d failed", failed))) } cli.Blank() @@ -282,9 +280,9 @@ func detectDefaultBranch(path string) string { // Check what origin/HEAD points to out, err := exec.Command("git", "-C", path, "symbolic-ref", "refs/remotes/origin/HEAD").Output() if err == nil { - ref := strings.TrimSpace(string(out)) + ref := core.Trim(string(out)) // refs/remotes/origin/main -> main - if parts := strings.Split(ref, "/"); len(parts) > 0 { + if parts := core.Split(ref, "/"); len(parts) > 0 { return parts[len(parts)-1] } } @@ -292,7 +290,7 @@ func detectDefaultBranch(path string) string { // Fallback: check current branch out, err = exec.Command("git", "-C", path, "branch", "--show-current").Output() if err == nil { - branch := strings.TrimSpace(string(out)) + branch := core.Trim(string(out)) if branch != "" { return branch } @@ -307,7 +305,7 @@ func configureGiteaRemote(localPath, remoteURL string) error { out, err := exec.Command("git", "-C", localPath, "remote", "get-url", "gitea").Output() if err == nil { // Remote exists — update if URL changed - existing := strings.TrimSpace(string(out)) + existing := core.Trim(string(out)) if existing != remoteURL { cmd := exec.Command("git", "-C", localPath, "remote", "set-url", "gitea", remoteURL) if err := cmd.Run(); err != nil { @@ -329,11 +327,11 @@ func configureGiteaRemote(localPath, remoteURL string) error { // pushUpstream pushes the local default branch to Gitea as 'upstream'. func pushUpstream(localPath, defaultBranch string) error { // Push origin's default branch as 'upstream' to gitea - refspec := fmt.Sprintf("refs/remotes/origin/%s:refs/heads/upstream", defaultBranch) + refspec := core.Sprintf("refs/remotes/origin/%s:refs/heads/upstream", defaultBranch) cmd := exec.Command("git", "-C", localPath, "push", "--force", "gitea", refspec) output, err := cmd.CombinedOutput() if err != nil { - return coreerr.E("gitea.pushUpstream", strings.TrimSpace(string(output)), err) + return coreerr.E("gitea.pushUpstream", core.Trim(string(output)), err) } return nil @@ -344,7 +342,7 @@ func gitFetch(localPath, remote string) error { cmd := exec.Command("git", "-C", localPath, "fetch", remote) output, err := cmd.CombinedOutput() if err != nil { - return coreerr.E("gitea.gitFetch", strings.TrimSpace(string(output)), err) + return coreerr.E("gitea.gitFetch", core.Trim(string(output)), err) } return nil } @@ -370,7 +368,7 @@ func repoNameFromArg(arg string) (string, error) { return "", coreerr.E("gitea.repoNameFromArg", "decode repo argument", err) } - parts := strings.Split(decoded, "/") + parts := core.Split(decoded, "/") switch len(parts) { case 1: return agentci.ValidatePathElement(parts[0]) diff --git a/cmd/gitea/cmd_sync_test.go b/cmd/gitea/cmd_sync_test.go index e21e712..71f21c9 100644 --- a/cmd/gitea/cmd_sync_test.go +++ b/cmd/gitea/cmd_sync_test.go @@ -1,7 +1,7 @@ package gitea import ( - "path/filepath" + core "dappco.re/go/core" "testing" "github.com/stretchr/testify/assert" @@ -9,17 +9,17 @@ import ( ) func TestBuildRepoList_Good(t *testing.T) { - basePath := filepath.Join(t.TempDir(), "repos") + basePath := core.JoinPath(t.TempDir(), "repos") repos, err := buildRepoList(nil, []string{"host-uk/core"}, basePath) require.NoError(t, err) require.Len(t, repos, 1) assert.Equal(t, "core", repos[0].name) - assert.Equal(t, filepath.Join(basePath, "core"), repos[0].localPath) + assert.Equal(t, core.JoinPath(basePath, "core"), repos[0].localPath) } func TestBuildRepoList_Bad_PathTraversal(t *testing.T) { - basePath := filepath.Join(t.TempDir(), "repos") + basePath := core.JoinPath(t.TempDir(), "repos") _, err := buildRepoList(nil, []string{"../escape"}, basePath) require.Error(t, err) @@ -27,17 +27,17 @@ func TestBuildRepoList_Bad_PathTraversal(t *testing.T) { } func TestBuildRepoList_Good_OwnerRepo(t *testing.T) { - basePath := filepath.Join(t.TempDir(), "repos") + basePath := core.JoinPath(t.TempDir(), "repos") repos, err := buildRepoList(nil, []string{"Host-UK/core"}, basePath) require.NoError(t, err) require.Len(t, repos, 1) assert.Equal(t, "core", repos[0].name) - assert.Equal(t, filepath.Join(basePath, "core"), repos[0].localPath) + assert.Equal(t, core.JoinPath(basePath, "core"), repos[0].localPath) } func TestBuildRepoList_Bad_PathTraversal_OwnerRepo(t *testing.T) { - basePath := filepath.Join(t.TempDir(), "repos") + basePath := core.JoinPath(t.TempDir(), "repos") _, err := buildRepoList(nil, []string{"host-uk/../escape"}, basePath) require.Error(t, err) @@ -45,7 +45,7 @@ func TestBuildRepoList_Bad_PathTraversal_OwnerRepo(t *testing.T) { } func TestBuildRepoList_Bad_PathTraversal_OwnerRepoEncoded(t *testing.T) { - basePath := filepath.Join(t.TempDir(), "repos") + basePath := core.JoinPath(t.TempDir(), "repos") _, err := buildRepoList(nil, []string{"host-uk%2F..%2Fescape"}, basePath) require.Error(t, err) diff --git a/cmd/scm/cmd_compile.go b/cmd/scm/cmd_compile.go index e5cce30..65309ce 100644 --- a/cmd/scm/cmd_compile.go +++ b/cmd/scm/cmd_compile.go @@ -4,9 +4,9 @@ import ( "crypto/ed25519" "encoding/hex" "os/exec" - "strings" "forge.lthn.ai/core/cli/pkg/cli" + core "dappco.re/go/core" "dappco.re/go/core/io" "dappco.re/go/core/scm/manifest" ) @@ -89,7 +89,7 @@ func gitCommit(dir string) string { if err != nil { return "" } - return strings.TrimSpace(string(out)) + return core.Trim(string(out)) } // gitTag returns the tag pointing at HEAD, or empty if none. @@ -98,5 +98,5 @@ func gitTag(dir string) string { if err != nil { return "" } - return strings.TrimSpace(string(out)) + return core.Trim(string(out)) } diff --git a/cmd/scm/cmd_index.go b/cmd/scm/cmd_index.go index dd8784b..b90bd1c 100644 --- a/cmd/scm/cmd_index.go +++ b/cmd/scm/cmd_index.go @@ -1,10 +1,10 @@ package scm import ( - "fmt" "path/filepath" "forge.lthn.ai/core/cli/pkg/cli" + core "dappco.re/go/core" "dappco.re/go/core/scm/marketplace" ) @@ -54,7 +54,7 @@ func runIndex(dirs []string, output, baseURL, org string) error { cli.Blank() cli.Print(" %s %s\n", successStyle.Render("index built"), valueStyle.Render(output)) - cli.Print(" %s %s\n", dimStyle.Render("modules:"), numberStyle.Render(fmt.Sprintf("%d", len(idx.Modules)))) + cli.Print(" %s %s\n", dimStyle.Render("modules:"), numberStyle.Render(core.Sprintf("%d", len(idx.Modules)))) cli.Blank() return nil diff --git a/collect/bitcointalk.go b/collect/bitcointalk.go index 8c189d6..b56e24b 100644 --- a/collect/bitcointalk.go +++ b/collect/bitcointalk.go @@ -2,14 +2,13 @@ package collect import ( "context" - "fmt" "iter" "net/http" - "path/filepath" "strings" "time" - core "dappco.re/go/core/log" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" "golang.org/x/net/html" ) @@ -38,7 +37,7 @@ func (b *BitcoinTalkCollector) Name() string { if id == "" && b.URL != "" { id = "url" } - return fmt.Sprintf("bitcointalk:%s", id) + return core.Sprintf("bitcointalk:%s", id) } // Collect gathers posts from a BitcoinTalk topic. @@ -51,19 +50,19 @@ func (b *BitcoinTalkCollector) Collect(ctx context.Context, cfg *Config) (*Resul topicID := b.TopicID if topicID == "" { - return result, core.E("collect.BitcoinTalk.Collect", "topic ID is required", nil) + return result, coreerr.E("collect.BitcoinTalk.Collect", "topic ID is required", nil) } if cfg.DryRun { if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitProgress(b.Name(), fmt.Sprintf("[dry-run] Would collect topic %s", topicID), nil) + cfg.Dispatcher.EmitProgress(b.Name(), core.Sprintf("[dry-run] Would collect topic %s", topicID), nil) } return result, nil } - baseDir := filepath.Join(cfg.OutputDir, "bitcointalk", topicID, "posts") + baseDir := core.JoinPath(cfg.OutputDir, "bitcointalk", topicID, "posts") if err := cfg.Output.EnsureDir(baseDir); err != nil { - return result, core.E("collect.BitcoinTalk.Collect", "failed to create output directory", err) + return result, coreerr.E("collect.BitcoinTalk.Collect", "failed to create output directory", err) } postNum := 0 @@ -73,7 +72,7 @@ func (b *BitcoinTalkCollector) Collect(ctx context.Context, cfg *Config) (*Resul for { if ctx.Err() != nil { - return result, core.E("collect.BitcoinTalk.Collect", "context cancelled", ctx.Err()) + return result, coreerr.E("collect.BitcoinTalk.Collect", "context cancelled", ctx.Err()) } if b.Pages > 0 && pageCount >= b.Pages { @@ -86,13 +85,13 @@ func (b *BitcoinTalkCollector) Collect(ctx context.Context, cfg *Config) (*Resul } } - pageURL := fmt.Sprintf("https://bitcointalk.org/index.php?topic=%s.%d", topicID, offset) + pageURL := core.Sprintf("https://bitcointalk.org/index.php?topic=%s.%d", topicID, offset) posts, err := b.fetchPage(ctx, pageURL) if err != nil { result.Errors++ if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitError(b.Name(), fmt.Sprintf("Failed to fetch page at offset %d: %v", offset, err), nil) + cfg.Dispatcher.EmitError(b.Name(), core.Sprintf("Failed to fetch page at offset %d: %v", offset, err), nil) } break } @@ -103,7 +102,7 @@ func (b *BitcoinTalkCollector) Collect(ctx context.Context, cfg *Config) (*Resul for _, post := range posts { postNum++ - filePath := filepath.Join(baseDir, fmt.Sprintf("%d.md", postNum)) + filePath := core.JoinPath(baseDir, core.Sprintf("%d.md", postNum)) content := formatPostMarkdown(postNum, post) if err := cfg.Output.Write(filePath, content); err != nil { @@ -115,7 +114,7 @@ func (b *BitcoinTalkCollector) Collect(ctx context.Context, cfg *Config) (*Resul result.Files = append(result.Files, filePath) if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitItem(b.Name(), fmt.Sprintf("Post %d by %s", postNum, post.Author), nil) + cfg.Dispatcher.EmitItem(b.Name(), core.Sprintf("Post %d by %s", postNum, post.Author), nil) } } @@ -129,7 +128,7 @@ func (b *BitcoinTalkCollector) Collect(ctx context.Context, cfg *Config) (*Resul } if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitComplete(b.Name(), fmt.Sprintf("Collected %d posts", result.Items), result) + cfg.Dispatcher.EmitComplete(b.Name(), core.Sprintf("Collected %d posts", result.Items), result) } return result, nil @@ -146,24 +145,24 @@ type btPost struct { func (b *BitcoinTalkCollector) fetchPage(ctx context.Context, pageURL string) ([]btPost, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, pageURL, nil) if err != nil { - return nil, core.E("collect.BitcoinTalk.fetchPage", "failed to create request", err) + return nil, coreerr.E("collect.BitcoinTalk.fetchPage", "failed to create request", err) } req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; CoreCollector/1.0)") resp, err := httpClient.Do(req) if err != nil { - return nil, core.E("collect.BitcoinTalk.fetchPage", "request failed", err) + return nil, coreerr.E("collect.BitcoinTalk.fetchPage", "request failed", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - return nil, core.E("collect.BitcoinTalk.fetchPage", - fmt.Sprintf("unexpected status code: %d", resp.StatusCode), nil) + return nil, coreerr.E("collect.BitcoinTalk.fetchPage", + core.Sprintf("unexpected status code: %d", resp.StatusCode), nil) } doc, err := html.Parse(resp.Body) if err != nil { - return nil, core.E("collect.BitcoinTalk.fetchPage", "failed to parse HTML", err) + return nil, coreerr.E("collect.BitcoinTalk.fetchPage", "failed to parse HTML", err) } return extractPosts(doc), nil @@ -186,7 +185,7 @@ func extractPostsIter(doc *html.Node) iter.Seq[btPost] { walk = func(n *html.Node) bool { if n.Type == html.ElementNode && n.Data == "div" { for _, attr := range n.Attr { - if attr.Key == "class" && strings.Contains(attr.Val, "post") { + if attr.Key == "class" && core.Contains(attr.Val, "post") { post := parsePost(n) if post.Content != "" { if !yield(post) { @@ -217,21 +216,21 @@ func parsePost(node *html.Node) btPost { for _, attr := range n.Attr { if attr.Key == "class" { switch { - case strings.Contains(attr.Val, "poster_info"): + case core.Contains(attr.Val, "poster_info"): post.Author = extractText(n) - case strings.Contains(attr.Val, "headerandpost"): + case core.Contains(attr.Val, "headerandpost"): // Look for date in smalltext for c := n.FirstChild; c != nil; c = c.NextSibling { if c.Type == html.ElementNode && c.Data == "div" { for _, a := range c.Attr { - if a.Key == "class" && strings.Contains(a.Val, "smalltext") { - post.Date = strings.TrimSpace(extractText(c)) + if a.Key == "class" && core.Contains(a.Val, "smalltext") { + post.Date = core.Trim(extractText(c)) } } } } - case strings.Contains(attr.Val, "inner"): - post.Content = strings.TrimSpace(extractText(n)) + case core.Contains(attr.Val, "inner"): + post.Content = core.Trim(extractText(n)) } } } @@ -267,10 +266,10 @@ func extractText(n *html.Node) string { // formatPostMarkdown formats a BitcoinTalk post as markdown. func formatPostMarkdown(num int, post btPost) string { var b strings.Builder - fmt.Fprintf(&b, "# Post %d by %s\n\n", num, post.Author) + b.WriteString(core.Sprintf("# Post %d by %s\n\n", num, post.Author)) if post.Date != "" { - fmt.Fprintf(&b, "**Date:** %s\n\n", post.Date) + b.WriteString(core.Sprintf("**Date:** %s\n\n", post.Date)) } b.WriteString(post.Content) @@ -282,9 +281,9 @@ func formatPostMarkdown(num int, post btPost) string { // ParsePostsFromHTML parses BitcoinTalk posts from raw HTML content. // This is exported for testing purposes. func ParsePostsFromHTML(htmlContent string) ([]btPost, error) { - doc, err := html.Parse(strings.NewReader(htmlContent)) + doc, err := html.Parse(core.NewReader(htmlContent)) if err != nil { - return nil, core.E("collect.ParsePostsFromHTML", "failed to parse HTML", err) + return nil, coreerr.E("collect.ParsePostsFromHTML", "failed to parse HTML", err) } return extractPosts(doc), nil } diff --git a/collect/bitcointalk_http_test.go b/collect/bitcointalk_http_test.go index 61a7a08..3591008 100644 --- a/collect/bitcointalk_http_test.go +++ b/collect/bitcointalk_http_test.go @@ -2,12 +2,12 @@ package collect import ( "context" - "fmt" "net/http" "net/http/httptest" "strings" "testing" + core "dappco.re/go/core" "dappco.re/go/core/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,7 +20,7 @@ func sampleBTCTalkPage(count int) string { var page strings.Builder page.WriteString(``) for i := range count { - page.WriteString(fmt.Sprintf(` + page.WriteString(core.Sprintf(`
user%d
@@ -66,10 +66,10 @@ func TestBitcoinTalkCollector_Collect_Good_OnePage(t *testing.T) { // Verify files were written. for i := 1; i <= 5; i++ { - path := fmt.Sprintf("/output/bitcointalk/12345/posts/%d.md", i) + path := core.Sprintf("/output/bitcointalk/12345/posts/%d.md", i) content, err := m.Read(path) require.NoError(t, err, "file %s should exist", path) - assert.Contains(t, content, fmt.Sprintf("Post %d by", i)) + assert.Contains(t, content, core.Sprintf("Post %d by", i)) } } diff --git a/collect/collect.go b/collect/collect.go index d1acd04..b64446d 100644 --- a/collect/collect.go +++ b/collect/collect.go @@ -6,8 +6,8 @@ package collect import ( "context" - "path/filepath" + core "dappco.re/go/core" "dappco.re/go/core/io" ) @@ -71,7 +71,7 @@ func NewConfig(outputDir string) *Config { Output: m, OutputDir: outputDir, Limiter: NewRateLimiter(), - State: NewState(m, filepath.Join(outputDir, ".collect-state.json")), + State: NewState(m, core.JoinPath(outputDir, ".collect-state.json")), Dispatcher: NewDispatcher(), } } @@ -82,7 +82,7 @@ func NewConfigWithMedium(m io.Medium, outputDir string) *Config { Output: m, OutputDir: outputDir, Limiter: NewRateLimiter(), - State: NewState(m, filepath.Join(outputDir, ".collect-state.json")), + State: NewState(m, core.JoinPath(outputDir, ".collect-state.json")), Dispatcher: NewDispatcher(), } } diff --git a/collect/coverage_phase2_test.go b/collect/coverage_phase2_test.go index 68b5469..96f0a73 100644 --- a/collect/coverage_phase2_test.go +++ b/collect/coverage_phase2_test.go @@ -8,10 +8,10 @@ import ( "io/fs" "net/http" "net/http/httptest" - "strings" "testing" "time" + core "dappco.re/go/core" "dappco.re/go/core/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -433,7 +433,7 @@ func TestPapersCollector_CollectArXiv_Good_EmitsItemEvents(t *testing.T) { func TestMarketCollector_Collect_Bad_WriteError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - if strings.Contains(r.URL.Path, "/market_chart") { + if core.Contains(r.URL.Path, "/market_chart") { _ = json.NewEncoder(w).Encode(historicalData{ Prices: [][]float64{{1610000000000, 42000.0}}, }) @@ -519,7 +519,7 @@ func TestMarketCollector_Collect_Bad_LimiterError(t *testing.T) { func TestMarketCollector_Collect_Good_HistoricalCustomDate(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - if strings.Contains(r.URL.Path, "/market_chart") { + if core.Contains(r.URL.Path, "/market_chart") { _ = json.NewEncoder(w).Encode(historicalData{ Prices: [][]float64{{1610000000000, 42000.0}}, }) @@ -752,7 +752,7 @@ func TestGitHubCollector_Collect_Bad_PRsOnlyGhFails(t *testing.T) { func TestExtractText_Good_TextBeforeBR(t *testing.T) { htmlStr := `
Hello
World

End

` - posts, err := ParsePostsFromHTML(fmt.Sprintf(`
%s
`, + posts, err := ParsePostsFromHTML(core.Sprintf(`
%s
`, "First text
Second text
Third text
")) // ParsePostsFromHTML uses extractText internally require.NoError(t, err) @@ -1001,7 +1001,7 @@ func (w *writeCountMedium) IsDir(path string) bool { return w. func TestMarketCollector_Collect_Bad_SummaryWriteError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - if strings.Contains(r.URL.Path, "/market_chart") { + if core.Contains(r.URL.Path, "/market_chart") { _ = json.NewEncoder(w).Encode(historicalData{ Prices: [][]float64{{1610000000000, 42000.0}}, }) @@ -1040,7 +1040,7 @@ func TestMarketCollector_Collect_Bad_HistoricalWriteError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { callCount++ w.Header().Set("Content-Type", "application/json") - if strings.Contains(r.URL.Path, "/market_chart") { + if core.Contains(r.URL.Path, "/market_chart") { _ = json.NewEncoder(w).Encode(historicalData{ Prices: [][]float64{{1610000000000, 42000.0}}, }) diff --git a/collect/excavate.go b/collect/excavate.go index e491ba3..7f09258 100644 --- a/collect/excavate.go +++ b/collect/excavate.go @@ -2,10 +2,10 @@ package collect import ( "context" - "fmt" "time" - core "dappco.re/go/core/log" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" ) // Excavator runs multiple collectors as a coordinated operation. @@ -37,13 +37,13 @@ func (e *Excavator) Run(ctx context.Context, cfg *Config) (*Result, error) { } if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitStart(e.Name(), fmt.Sprintf("Starting excavation with %d collectors", len(e.Collectors))) + cfg.Dispatcher.EmitStart(e.Name(), core.Sprintf("Starting excavation with %d collectors", len(e.Collectors))) } // Load state if resuming if e.Resume && cfg.State != nil { if err := cfg.State.Load(); err != nil { - return result, core.E("collect.Excavator.Run", "failed to load state", err) + return result, coreerr.E("collect.Excavator.Run", "failed to load state", err) } } @@ -51,7 +51,7 @@ func (e *Excavator) Run(ctx context.Context, cfg *Config) (*Result, error) { if e.ScanOnly { for _, c := range e.Collectors { if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitProgress(e.Name(), fmt.Sprintf("[scan] Would run collector: %s", c.Name()), nil) + cfg.Dispatcher.EmitProgress(e.Name(), core.Sprintf("[scan] Would run collector: %s", c.Name()), nil) } } return result, nil @@ -59,12 +59,12 @@ func (e *Excavator) Run(ctx context.Context, cfg *Config) (*Result, error) { for i, c := range e.Collectors { if ctx.Err() != nil { - return result, core.E("collect.Excavator.Run", "context cancelled", ctx.Err()) + return result, coreerr.E("collect.Excavator.Run", "context cancelled", ctx.Err()) } if cfg.Dispatcher != nil { cfg.Dispatcher.EmitProgress(e.Name(), - fmt.Sprintf("Running collector %d/%d: %s", i+1, len(e.Collectors), c.Name()), nil) + core.Sprintf("Running collector %d/%d: %s", i+1, len(e.Collectors), c.Name()), nil) } // Check if we should skip (already completed in a previous run) @@ -73,7 +73,7 @@ func (e *Excavator) Run(ctx context.Context, cfg *Config) (*Result, error) { if entry.Items > 0 && !entry.LastRun.IsZero() { if cfg.Dispatcher != nil { cfg.Dispatcher.EmitProgress(e.Name(), - fmt.Sprintf("Skipping %s (already collected %d items on %s)", + core.Sprintf("Skipping %s (already collected %d items on %s)", c.Name(), entry.Items, entry.LastRun.Format(time.RFC3339)), nil) } result.Skipped++ @@ -87,7 +87,7 @@ func (e *Excavator) Run(ctx context.Context, cfg *Config) (*Result, error) { result.Errors++ if cfg.Dispatcher != nil { cfg.Dispatcher.EmitError(e.Name(), - fmt.Sprintf("Collector %s failed: %v", c.Name(), err), nil) + core.Sprintf("Collector %s failed: %v", c.Name(), err), nil) } continue } @@ -113,14 +113,14 @@ func (e *Excavator) Run(ctx context.Context, cfg *Config) (*Result, error) { if cfg.State != nil { if err := cfg.State.Save(); err != nil { if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitError(e.Name(), fmt.Sprintf("Failed to save state: %v", err), nil) + cfg.Dispatcher.EmitError(e.Name(), core.Sprintf("Failed to save state: %v", err), nil) } } } if cfg.Dispatcher != nil { cfg.Dispatcher.EmitComplete(e.Name(), - fmt.Sprintf("Excavation complete: %d items, %d errors, %d skipped", + core.Sprintf("Excavation complete: %d items, %d errors, %d skipped", result.Items, result.Errors, result.Skipped), result) } diff --git a/collect/excavate_test.go b/collect/excavate_test.go index 0bebb30..cd2477a 100644 --- a/collect/excavate_test.go +++ b/collect/excavate_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + core "dappco.re/go/core" "dappco.re/go/core/io" "github.com/stretchr/testify/assert" ) @@ -27,7 +28,7 @@ func (m *mockCollector) Collect(ctx context.Context, cfg *Config) (*Result, erro result := &Result{Source: m.name, Items: m.items} for i := range m.items { - result.Files = append(result.Files, fmt.Sprintf("/output/%s/%d.md", m.name, i)) + result.Files = append(result.Files, core.Sprintf("/output/%s/%d.md", m.name, i)) } if cfg.DryRun { diff --git a/collect/github.go b/collect/github.go index cad8fa7..f70aa2a 100644 --- a/collect/github.go +++ b/collect/github.go @@ -2,14 +2,14 @@ package collect import ( "context" - "encoding/json" - "fmt" "os/exec" - "path/filepath" "strings" "time" - core "dappco.re/go/core/log" + "encoding/json" + + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" ) // ghIssue represents a GitHub issue or pull request as returned by the gh CLI. @@ -55,9 +55,9 @@ type GitHubCollector struct { // Name returns the collector name. func (g *GitHubCollector) Name() string { if g.Repo != "" { - return fmt.Sprintf("github:%s/%s", g.Org, g.Repo) + return core.Sprintf("github:%s/%s", g.Org, g.Repo) } - return fmt.Sprintf("github:%s", g.Org) + return core.Sprintf("github:%s", g.Org) } // Collect gathers issues and/or PRs from GitHub repositories. @@ -80,7 +80,7 @@ func (g *GitHubCollector) Collect(ctx context.Context, cfg *Config) (*Result, er for _, repo := range repos { if ctx.Err() != nil { - return result, core.E("collect.GitHub.Collect", "context cancelled", ctx.Err()) + return result, coreerr.E("collect.GitHub.Collect", "context cancelled", ctx.Err()) } if !g.PRsOnly { @@ -88,7 +88,7 @@ func (g *GitHubCollector) Collect(ctx context.Context, cfg *Config) (*Result, er if err != nil { result.Errors++ if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitError(g.Name(), fmt.Sprintf("Error collecting issues for %s: %v", repo, err), nil) + cfg.Dispatcher.EmitError(g.Name(), core.Sprintf("Error collecting issues for %s: %v", repo, err), nil) } } else { result.Items += issueResult.Items @@ -102,7 +102,7 @@ func (g *GitHubCollector) Collect(ctx context.Context, cfg *Config) (*Result, er if err != nil { result.Errors++ if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitError(g.Name(), fmt.Sprintf("Error collecting PRs for %s: %v", repo, err), nil) + cfg.Dispatcher.EmitError(g.Name(), core.Sprintf("Error collecting PRs for %s: %v", repo, err), nil) } } else { result.Items += prResult.Items @@ -113,7 +113,7 @@ func (g *GitHubCollector) Collect(ctx context.Context, cfg *Config) (*Result, er } if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitComplete(g.Name(), fmt.Sprintf("Collected %d items", result.Items), result) + cfg.Dispatcher.EmitComplete(g.Name(), core.Sprintf("Collected %d items", result.Items), result) } return result, nil @@ -127,12 +127,12 @@ func (g *GitHubCollector) listOrgRepos(ctx context.Context) ([]string, error) { ) out, err := cmd.Output() if err != nil { - return nil, core.E("collect.GitHub.listOrgRepos", "failed to list repos", err) + return nil, coreerr.E("collect.GitHub.listOrgRepos", "failed to list repos", err) } var repos []ghRepo if err := json.Unmarshal(out, &repos); err != nil { - return nil, core.E("collect.GitHub.listOrgRepos", "failed to parse repo list", err) + return nil, coreerr.E("collect.GitHub.listOrgRepos", "failed to parse repo list", err) } names := make([]string, len(repos)) @@ -144,11 +144,11 @@ func (g *GitHubCollector) listOrgRepos(ctx context.Context) ([]string, error) { // collectIssues collects issues for a single repository. func (g *GitHubCollector) collectIssues(ctx context.Context, cfg *Config, repo string) (*Result, error) { - result := &Result{Source: fmt.Sprintf("github:%s/%s/issues", g.Org, repo)} + result := &Result{Source: core.Sprintf("github:%s/%s/issues", g.Org, repo)} if cfg.DryRun { if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitProgress(g.Name(), fmt.Sprintf("[dry-run] Would collect issues for %s/%s", g.Org, repo), nil) + cfg.Dispatcher.EmitProgress(g.Name(), core.Sprintf("[dry-run] Would collect issues for %s/%s", g.Org, repo), nil) } return result, nil } @@ -159,7 +159,7 @@ func (g *GitHubCollector) collectIssues(ctx context.Context, cfg *Config, repo s } } - repoRef := fmt.Sprintf("%s/%s", g.Org, repo) + repoRef := core.Sprintf("%s/%s", g.Org, repo) cmd := exec.CommandContext(ctx, "gh", "issue", "list", "--repo", repoRef, "--json", "number,title,state,author,body,createdAt,labels,url", @@ -168,21 +168,21 @@ func (g *GitHubCollector) collectIssues(ctx context.Context, cfg *Config, repo s ) out, err := cmd.Output() if err != nil { - return result, core.E("collect.GitHub.collectIssues", "gh issue list failed for "+repoRef, err) + return result, coreerr.E("collect.GitHub.collectIssues", "gh issue list failed for "+repoRef, err) } var issues []ghIssue if err := json.Unmarshal(out, &issues); err != nil { - return result, core.E("collect.GitHub.collectIssues", "failed to parse issues", err) + return result, coreerr.E("collect.GitHub.collectIssues", "failed to parse issues", err) } - baseDir := filepath.Join(cfg.OutputDir, "github", g.Org, repo, "issues") + baseDir := core.JoinPath(cfg.OutputDir, "github", g.Org, repo, "issues") if err := cfg.Output.EnsureDir(baseDir); err != nil { - return result, core.E("collect.GitHub.collectIssues", "failed to create output directory", err) + return result, coreerr.E("collect.GitHub.collectIssues", "failed to create output directory", err) } for _, issue := range issues { - filePath := filepath.Join(baseDir, fmt.Sprintf("%d.md", issue.Number)) + filePath := core.JoinPath(baseDir, core.Sprintf("%d.md", issue.Number)) content := formatIssueMarkdown(issue) if err := cfg.Output.Write(filePath, content); err != nil { @@ -194,7 +194,7 @@ func (g *GitHubCollector) collectIssues(ctx context.Context, cfg *Config, repo s result.Files = append(result.Files, filePath) if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitItem(g.Name(), fmt.Sprintf("Issue #%d: %s", issue.Number, issue.Title), nil) + cfg.Dispatcher.EmitItem(g.Name(), core.Sprintf("Issue #%d: %s", issue.Number, issue.Title), nil) } } @@ -203,11 +203,11 @@ func (g *GitHubCollector) collectIssues(ctx context.Context, cfg *Config, repo s // collectPRs collects pull requests for a single repository. func (g *GitHubCollector) collectPRs(ctx context.Context, cfg *Config, repo string) (*Result, error) { - result := &Result{Source: fmt.Sprintf("github:%s/%s/pulls", g.Org, repo)} + result := &Result{Source: core.Sprintf("github:%s/%s/pulls", g.Org, repo)} if cfg.DryRun { if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitProgress(g.Name(), fmt.Sprintf("[dry-run] Would collect PRs for %s/%s", g.Org, repo), nil) + cfg.Dispatcher.EmitProgress(g.Name(), core.Sprintf("[dry-run] Would collect PRs for %s/%s", g.Org, repo), nil) } return result, nil } @@ -218,7 +218,7 @@ func (g *GitHubCollector) collectPRs(ctx context.Context, cfg *Config, repo stri } } - repoRef := fmt.Sprintf("%s/%s", g.Org, repo) + repoRef := core.Sprintf("%s/%s", g.Org, repo) cmd := exec.CommandContext(ctx, "gh", "pr", "list", "--repo", repoRef, "--json", "number,title,state,author,body,createdAt,labels,url", @@ -227,21 +227,21 @@ func (g *GitHubCollector) collectPRs(ctx context.Context, cfg *Config, repo stri ) out, err := cmd.Output() if err != nil { - return result, core.E("collect.GitHub.collectPRs", "gh pr list failed for "+repoRef, err) + return result, coreerr.E("collect.GitHub.collectPRs", "gh pr list failed for "+repoRef, err) } var prs []ghIssue if err := json.Unmarshal(out, &prs); err != nil { - return result, core.E("collect.GitHub.collectPRs", "failed to parse pull requests", err) + return result, coreerr.E("collect.GitHub.collectPRs", "failed to parse pull requests", err) } - baseDir := filepath.Join(cfg.OutputDir, "github", g.Org, repo, "pulls") + baseDir := core.JoinPath(cfg.OutputDir, "github", g.Org, repo, "pulls") if err := cfg.Output.EnsureDir(baseDir); err != nil { - return result, core.E("collect.GitHub.collectPRs", "failed to create output directory", err) + return result, coreerr.E("collect.GitHub.collectPRs", "failed to create output directory", err) } for _, pr := range prs { - filePath := filepath.Join(baseDir, fmt.Sprintf("%d.md", pr.Number)) + filePath := core.JoinPath(baseDir, core.Sprintf("%d.md", pr.Number)) content := formatIssueMarkdown(pr) if err := cfg.Output.Write(filePath, content); err != nil { @@ -253,7 +253,7 @@ func (g *GitHubCollector) collectPRs(ctx context.Context, cfg *Config, repo stri result.Files = append(result.Files, filePath) if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitItem(g.Name(), fmt.Sprintf("PR #%d: %s", pr.Number, pr.Title), nil) + cfg.Dispatcher.EmitItem(g.Name(), core.Sprintf("PR #%d: %s", pr.Number, pr.Title), nil) } } @@ -263,26 +263,26 @@ func (g *GitHubCollector) collectPRs(ctx context.Context, cfg *Config, repo stri // formatIssueMarkdown formats a GitHub issue or PR as markdown. func formatIssueMarkdown(issue ghIssue) string { var b strings.Builder - fmt.Fprintf(&b, "# %s\n\n", issue.Title) - fmt.Fprintf(&b, "- **Number:** #%d\n", issue.Number) - fmt.Fprintf(&b, "- **State:** %s\n", issue.State) - fmt.Fprintf(&b, "- **Author:** %s\n", issue.Author.Login) - fmt.Fprintf(&b, "- **Created:** %s\n", issue.CreatedAt.Format(time.RFC3339)) + b.WriteString(core.Sprintf("# %s\n\n", issue.Title)) + b.WriteString(core.Sprintf("- **Number:** #%d\n", issue.Number)) + b.WriteString(core.Sprintf("- **State:** %s\n", issue.State)) + b.WriteString(core.Sprintf("- **Author:** %s\n", issue.Author.Login)) + b.WriteString(core.Sprintf("- **Created:** %s\n", issue.CreatedAt.Format(time.RFC3339))) if len(issue.Labels) > 0 { labels := make([]string, len(issue.Labels)) for i, l := range issue.Labels { labels[i] = l.Name } - fmt.Fprintf(&b, "- **Labels:** %s\n", strings.Join(labels, ", ")) + b.WriteString(core.Sprintf("- **Labels:** %s\n", core.Join(", ", labels...))) } if issue.URL != "" { - fmt.Fprintf(&b, "- **URL:** %s\n", issue.URL) + b.WriteString(core.Sprintf("- **URL:** %s\n", issue.URL)) } if issue.Body != "" { - fmt.Fprintf(&b, "\n%s\n", issue.Body) + b.WriteString(core.Sprintf("\n%s\n", issue.Body)) } return b.String() diff --git a/collect/market.go b/collect/market.go index e38e162..45b5da4 100644 --- a/collect/market.go +++ b/collect/market.go @@ -3,13 +3,12 @@ package collect import ( "context" "encoding/json" - "fmt" "net/http" - "path/filepath" "strings" "time" - core "dappco.re/go/core/log" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" ) // coinGeckoBaseURL is the base URL for the CoinGecko API. @@ -30,7 +29,7 @@ type MarketCollector struct { // Name returns the collector name. func (m *MarketCollector) Name() string { - return fmt.Sprintf("market:%s", m.CoinID) + return core.Sprintf("market:%s", m.CoinID) } // coinData represents the current coin data from CoinGecko. @@ -67,23 +66,23 @@ func (m *MarketCollector) Collect(ctx context.Context, cfg *Config) (*Result, er result := &Result{Source: m.Name()} if m.CoinID == "" { - return result, core.E("collect.Market.Collect", "coin ID is required", nil) + return result, coreerr.E("collect.Market.Collect", "coin ID is required", nil) } if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitStart(m.Name(), fmt.Sprintf("Starting market data collection for %s", m.CoinID)) + cfg.Dispatcher.EmitStart(m.Name(), core.Sprintf("Starting market data collection for %s", m.CoinID)) } if cfg.DryRun { if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitProgress(m.Name(), fmt.Sprintf("[dry-run] Would collect market data for %s", m.CoinID), nil) + cfg.Dispatcher.EmitProgress(m.Name(), core.Sprintf("[dry-run] Would collect market data for %s", m.CoinID), nil) } return result, nil } - baseDir := filepath.Join(cfg.OutputDir, "market", m.CoinID) + baseDir := core.JoinPath(cfg.OutputDir, "market", m.CoinID) if err := cfg.Output.EnsureDir(baseDir); err != nil { - return result, core.E("collect.Market.Collect", "failed to create output directory", err) + return result, coreerr.E("collect.Market.Collect", "failed to create output directory", err) } // Collect current data @@ -91,7 +90,7 @@ func (m *MarketCollector) Collect(ctx context.Context, cfg *Config) (*Result, er if err != nil { result.Errors++ if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitError(m.Name(), fmt.Sprintf("Failed to collect current data: %v", err), nil) + cfg.Dispatcher.EmitError(m.Name(), core.Sprintf("Failed to collect current data: %v", err), nil) } } else { result.Items += currentResult.Items @@ -104,7 +103,7 @@ func (m *MarketCollector) Collect(ctx context.Context, cfg *Config) (*Result, er if err != nil { result.Errors++ if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitError(m.Name(), fmt.Sprintf("Failed to collect historical data: %v", err), nil) + cfg.Dispatcher.EmitError(m.Name(), core.Sprintf("Failed to collect historical data: %v", err), nil) } } else { result.Items += histResult.Items @@ -113,7 +112,7 @@ func (m *MarketCollector) Collect(ctx context.Context, cfg *Config) (*Result, er } if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitComplete(m.Name(), fmt.Sprintf("Collected market data for %s", m.CoinID), result) + cfg.Dispatcher.EmitComplete(m.Name(), core.Sprintf("Collected market data for %s", m.CoinID), result) } return result, nil @@ -129,30 +128,30 @@ func (m *MarketCollector) collectCurrent(ctx context.Context, cfg *Config, baseD } } - url := fmt.Sprintf("%s/coins/%s", coinGeckoBaseURL, m.CoinID) + url := core.Sprintf("%s/coins/%s", coinGeckoBaseURL, m.CoinID) data, err := fetchJSON[coinData](ctx, url) if err != nil { - return result, core.E("collect.Market.collectCurrent", "failed to fetch coin data", err) + return result, coreerr.E("collect.Market.collectCurrent", "failed to fetch coin data", err) } // Write raw JSON jsonBytes, err := json.MarshalIndent(data, "", " ") if err != nil { - return result, core.E("collect.Market.collectCurrent", "failed to marshal data", err) + return result, coreerr.E("collect.Market.collectCurrent", "failed to marshal data", err) } - jsonPath := filepath.Join(baseDir, "current.json") + jsonPath := core.JoinPath(baseDir, "current.json") if err := cfg.Output.Write(jsonPath, string(jsonBytes)); err != nil { - return result, core.E("collect.Market.collectCurrent", "failed to write JSON", err) + return result, coreerr.E("collect.Market.collectCurrent", "failed to write JSON", err) } result.Items++ result.Files = append(result.Files, jsonPath) // Write summary markdown summary := formatMarketSummary(data) - summaryPath := filepath.Join(baseDir, "summary.md") + summaryPath := core.JoinPath(baseDir, "summary.md") if err := cfg.Output.Write(summaryPath, summary); err != nil { - return result, core.E("collect.Market.collectCurrent", "failed to write summary", err) + return result, coreerr.E("collect.Market.collectCurrent", "failed to write summary", err) } result.Items++ result.Files = append(result.Files, summaryPath) @@ -176,25 +175,25 @@ func (m *MarketCollector) collectHistorical(ctx context.Context, cfg *Config, ba if err == nil { dayCount := int(time.Since(fromTime).Hours() / 24) if dayCount > 0 { - days = fmt.Sprintf("%d", dayCount) + days = core.Sprintf("%d", dayCount) } } } - url := fmt.Sprintf("%s/coins/%s/market_chart?vs_currency=usd&days=%s", coinGeckoBaseURL, m.CoinID, days) + url := core.Sprintf("%s/coins/%s/market_chart?vs_currency=usd&days=%s", coinGeckoBaseURL, m.CoinID, days) data, err := fetchJSON[historicalData](ctx, url) if err != nil { - return result, core.E("collect.Market.collectHistorical", "failed to fetch historical data", err) + return result, coreerr.E("collect.Market.collectHistorical", "failed to fetch historical data", err) } jsonBytes, err := json.MarshalIndent(data, "", " ") if err != nil { - return result, core.E("collect.Market.collectHistorical", "failed to marshal data", err) + return result, coreerr.E("collect.Market.collectHistorical", "failed to marshal data", err) } - jsonPath := filepath.Join(baseDir, "historical.json") + jsonPath := core.JoinPath(baseDir, "historical.json") if err := cfg.Output.Write(jsonPath, string(jsonBytes)); err != nil { - return result, core.E("collect.Market.collectHistorical", "failed to write JSON", err) + return result, coreerr.E("collect.Market.collectHistorical", "failed to write JSON", err) } result.Items++ result.Files = append(result.Files, jsonPath) @@ -206,25 +205,25 @@ func (m *MarketCollector) collectHistorical(ctx context.Context, cfg *Config, ba func fetchJSON[T any](ctx context.Context, url string) (*T, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - return nil, core.E("collect.fetchJSON", "failed to create request", err) + return nil, coreerr.E("collect.fetchJSON", "failed to create request", err) } req.Header.Set("User-Agent", "CoreCollector/1.0") req.Header.Set("Accept", "application/json") resp, err := httpClient.Do(req) if err != nil { - return nil, core.E("collect.fetchJSON", "request failed", err) + return nil, coreerr.E("collect.fetchJSON", "request failed", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - return nil, core.E("collect.fetchJSON", - fmt.Sprintf("unexpected status code: %d for %s", resp.StatusCode, url), nil) + return nil, coreerr.E("collect.fetchJSON", + core.Sprintf("unexpected status code: %d for %s", resp.StatusCode, url), nil) } var data T if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { - return nil, core.E("collect.fetchJSON", "failed to decode response", err) + return nil, coreerr.E("collect.fetchJSON", "failed to decode response", err) } return &data, nil @@ -233,39 +232,39 @@ func fetchJSON[T any](ctx context.Context, url string) (*T, error) { // formatMarketSummary formats coin data as a markdown summary. func formatMarketSummary(data *coinData) string { var b strings.Builder - fmt.Fprintf(&b, "# %s (%s)\n\n", data.Name, strings.ToUpper(data.Symbol)) + b.WriteString(core.Sprintf("# %s (%s)\n\n", data.Name, core.Upper(data.Symbol))) md := data.MarketData if price, ok := md.CurrentPrice["usd"]; ok { - fmt.Fprintf(&b, "- **Current Price (USD):** $%.2f\n", price) + b.WriteString(core.Sprintf("- **Current Price (USD):** $%.2f\n", price)) } if cap, ok := md.MarketCap["usd"]; ok { - fmt.Fprintf(&b, "- **Market Cap (USD):** $%.0f\n", cap) + b.WriteString(core.Sprintf("- **Market Cap (USD):** $%.0f\n", cap)) } if vol, ok := md.TotalVolume["usd"]; ok { - fmt.Fprintf(&b, "- **24h Volume (USD):** $%.0f\n", vol) + b.WriteString(core.Sprintf("- **24h Volume (USD):** $%.0f\n", vol)) } if high, ok := md.High24h["usd"]; ok { - fmt.Fprintf(&b, "- **24h High (USD):** $%.2f\n", high) + b.WriteString(core.Sprintf("- **24h High (USD):** $%.2f\n", high)) } if low, ok := md.Low24h["usd"]; ok { - fmt.Fprintf(&b, "- **24h Low (USD):** $%.2f\n", low) + b.WriteString(core.Sprintf("- **24h Low (USD):** $%.2f\n", low)) } - fmt.Fprintf(&b, "- **24h Price Change:** $%.2f (%.2f%%)\n", md.PriceChange24h, md.PriceChangePct24h) + b.WriteString(core.Sprintf("- **24h Price Change:** $%.2f (%.2f%%)\n", md.PriceChange24h, md.PriceChangePct24h)) if md.MarketCapRank > 0 { - fmt.Fprintf(&b, "- **Market Cap Rank:** #%d\n", md.MarketCapRank) + b.WriteString(core.Sprintf("- **Market Cap Rank:** #%d\n", md.MarketCapRank)) } if md.CirculatingSupply > 0 { - fmt.Fprintf(&b, "- **Circulating Supply:** %.0f\n", md.CirculatingSupply) + b.WriteString(core.Sprintf("- **Circulating Supply:** %.0f\n", md.CirculatingSupply)) } if md.TotalSupply > 0 { - fmt.Fprintf(&b, "- **Total Supply:** %.0f\n", md.TotalSupply) + b.WriteString(core.Sprintf("- **Total Supply:** %.0f\n", md.TotalSupply)) } if md.LastUpdated != "" { - fmt.Fprintf(&b, "\n*Last updated: %s*\n", md.LastUpdated) + b.WriteString(core.Sprintf("\n*Last updated: %s*\n", md.LastUpdated)) } return b.String() diff --git a/collect/papers.go b/collect/papers.go index bfbf663..3b3b29b 100644 --- a/collect/papers.go +++ b/collect/papers.go @@ -3,14 +3,13 @@ package collect import ( "context" "encoding/xml" - "fmt" "iter" "net/http" "net/url" - "path/filepath" "strings" - core "dappco.re/go/core/log" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" "golang.org/x/net/html" ) @@ -35,7 +34,7 @@ type PapersCollector struct { // Name returns the collector name. func (p *PapersCollector) Name() string { - return fmt.Sprintf("papers:%s", p.Source) + return core.Sprintf("papers:%s", p.Source) } // paper represents a parsed academic paper. @@ -54,16 +53,16 @@ func (p *PapersCollector) Collect(ctx context.Context, cfg *Config) (*Result, er result := &Result{Source: p.Name()} if p.Query == "" { - return result, core.E("collect.Papers.Collect", "query is required", nil) + return result, coreerr.E("collect.Papers.Collect", "query is required", nil) } if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitStart(p.Name(), fmt.Sprintf("Starting paper collection for %q", p.Query)) + cfg.Dispatcher.EmitStart(p.Name(), core.Sprintf("Starting paper collection for %q", p.Query)) } if cfg.DryRun { if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitProgress(p.Name(), fmt.Sprintf("[dry-run] Would search papers for %q", p.Query), nil) + cfg.Dispatcher.EmitProgress(p.Name(), core.Sprintf("[dry-run] Would search papers for %q", p.Query), nil) } return result, nil } @@ -78,7 +77,7 @@ func (p *PapersCollector) Collect(ctx context.Context, cfg *Config) (*Result, er arxivResult, arxivErr := p.collectArXiv(ctx, cfg) if iacrErr != nil && arxivErr != nil { - return result, core.E("collect.Papers.Collect", "all sources failed", iacrErr) + return result, coreerr.E("collect.Papers.Collect", "all sources failed", iacrErr) } merged := MergeResults(p.Name(), iacrResult, arxivResult) @@ -90,13 +89,13 @@ func (p *PapersCollector) Collect(ctx context.Context, cfg *Config) (*Result, er } if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitComplete(p.Name(), fmt.Sprintf("Collected %d papers", merged.Items), merged) + cfg.Dispatcher.EmitComplete(p.Name(), core.Sprintf("Collected %d papers", merged.Items), merged) } return merged, nil default: - return result, core.E("collect.Papers.Collect", - fmt.Sprintf("unknown source: %s (use iacr, arxiv, or all)", p.Source), nil) + return result, coreerr.E("collect.Papers.Collect", + core.Sprintf("unknown source: %s (use iacr, arxiv, or all)", p.Source), nil) } } @@ -110,39 +109,39 @@ func (p *PapersCollector) collectIACR(ctx context.Context, cfg *Config) (*Result } } - searchURL := fmt.Sprintf("https://eprint.iacr.org/search?q=%s", url.QueryEscape(p.Query)) + searchURL := core.Sprintf("https://eprint.iacr.org/search?q=%s", url.QueryEscape(p.Query)) req, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil) if err != nil { - return result, core.E("collect.Papers.collectIACR", "failed to create request", err) + return result, coreerr.E("collect.Papers.collectIACR", "failed to create request", err) } req.Header.Set("User-Agent", "CoreCollector/1.0") resp, err := httpClient.Do(req) if err != nil { - return result, core.E("collect.Papers.collectIACR", "request failed", err) + return result, coreerr.E("collect.Papers.collectIACR", "request failed", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - return result, core.E("collect.Papers.collectIACR", - fmt.Sprintf("unexpected status code: %d", resp.StatusCode), nil) + return result, coreerr.E("collect.Papers.collectIACR", + core.Sprintf("unexpected status code: %d", resp.StatusCode), nil) } doc, err := html.Parse(resp.Body) if err != nil { - return result, core.E("collect.Papers.collectIACR", "failed to parse HTML", err) + return result, coreerr.E("collect.Papers.collectIACR", "failed to parse HTML", err) } papers := extractIACRPapers(doc) - baseDir := filepath.Join(cfg.OutputDir, "papers", "iacr") + baseDir := core.JoinPath(cfg.OutputDir, "papers", "iacr") if err := cfg.Output.EnsureDir(baseDir); err != nil { - return result, core.E("collect.Papers.collectIACR", "failed to create output directory", err) + return result, coreerr.E("collect.Papers.collectIACR", "failed to create output directory", err) } for _, ppr := range papers { - filePath := filepath.Join(baseDir, ppr.ID+".md") + filePath := core.JoinPath(baseDir, ppr.ID+".md") content := formatPaperMarkdown(ppr) if err := cfg.Output.Write(filePath, content); err != nil { @@ -154,7 +153,7 @@ func (p *PapersCollector) collectIACR(ctx context.Context, cfg *Config) (*Result result.Files = append(result.Files, filePath) if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitItem(p.Name(), fmt.Sprintf("Paper: %s", ppr.Title), nil) + cfg.Dispatcher.EmitItem(p.Name(), core.Sprintf("Paper: %s", ppr.Title), nil) } } @@ -198,42 +197,42 @@ func (p *PapersCollector) collectArXiv(ctx context.Context, cfg *Config) (*Resul query := url.QueryEscape(p.Query) if p.Category != "" { - query = fmt.Sprintf("cat:%s+AND+%s", url.QueryEscape(p.Category), query) + query = core.Sprintf("cat:%s+AND+%s", url.QueryEscape(p.Category), query) } - searchURL := fmt.Sprintf("https://export.arxiv.org/api/query?search_query=%s&max_results=50", query) + searchURL := core.Sprintf("https://export.arxiv.org/api/query?search_query=%s&max_results=50", query) req, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil) if err != nil { - return result, core.E("collect.Papers.collectArXiv", "failed to create request", err) + return result, coreerr.E("collect.Papers.collectArXiv", "failed to create request", err) } req.Header.Set("User-Agent", "CoreCollector/1.0") resp, err := httpClient.Do(req) if err != nil { - return result, core.E("collect.Papers.collectArXiv", "request failed", err) + return result, coreerr.E("collect.Papers.collectArXiv", "request failed", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - return result, core.E("collect.Papers.collectArXiv", - fmt.Sprintf("unexpected status code: %d", resp.StatusCode), nil) + return result, coreerr.E("collect.Papers.collectArXiv", + core.Sprintf("unexpected status code: %d", resp.StatusCode), nil) } var feed arxivFeed if err := xml.NewDecoder(resp.Body).Decode(&feed); err != nil { - return result, core.E("collect.Papers.collectArXiv", "failed to parse XML", err) + return result, coreerr.E("collect.Papers.collectArXiv", "failed to parse XML", err) } - baseDir := filepath.Join(cfg.OutputDir, "papers", "arxiv") + baseDir := core.JoinPath(cfg.OutputDir, "papers", "arxiv") if err := cfg.Output.EnsureDir(baseDir); err != nil { - return result, core.E("collect.Papers.collectArXiv", "failed to create output directory", err) + return result, coreerr.E("collect.Papers.collectArXiv", "failed to create output directory", err) } for _, entry := range feed.Entries { ppr := arxivEntryToPaper(entry) - filePath := filepath.Join(baseDir, ppr.ID+".md") + filePath := core.JoinPath(baseDir, ppr.ID+".md") content := formatPaperMarkdown(ppr) if err := cfg.Output.Write(filePath, content); err != nil { @@ -245,7 +244,7 @@ func (p *PapersCollector) collectArXiv(ctx context.Context, cfg *Config) (*Resul result.Files = append(result.Files, filePath) if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitItem(p.Name(), fmt.Sprintf("Paper: %s", ppr.Title), nil) + cfg.Dispatcher.EmitItem(p.Name(), core.Sprintf("Paper: %s", ppr.Title), nil) } } @@ -265,8 +264,8 @@ func arxivEntryToPaper(entry arxivEntry) paper { id = id[idx+5:] } // Replace characters that are not valid in file names - id = strings.ReplaceAll(id, "/", "-") - id = strings.ReplaceAll(id, ":", "-") + id = core.Replace(id, "/", "-") + id = core.Replace(id, ":", "-") paperURL := entry.ID for _, link := range entry.Links { @@ -278,9 +277,9 @@ func arxivEntryToPaper(entry arxivEntry) paper { return paper{ ID: id, - Title: strings.TrimSpace(entry.Title), + Title: core.Trim(entry.Title), Authors: authors, - Abstract: strings.TrimSpace(entry.Summary), + Abstract: core.Trim(entry.Summary), Date: entry.Published, URL: paperURL, Source: "arxiv", @@ -303,7 +302,7 @@ func extractIACRPapersIter(doc *html.Node) iter.Seq[paper] { walk = func(n *html.Node) bool { if n.Type == html.ElementNode && n.Data == "div" { for _, attr := range n.Attr { - if attr.Key == "class" && strings.Contains(attr.Val, "paperentry") { + if attr.Key == "class" && core.Contains(attr.Val, "paperentry") { ppr := parseIACREntry(n) if ppr.Title != "" { if !yield(ppr) { @@ -334,36 +333,36 @@ func parseIACREntry(node *html.Node) paper { switch n.Data { case "a": for _, attr := range n.Attr { - if attr.Key == "href" && strings.Contains(attr.Val, "/eprint/") { + if attr.Key == "href" && core.Contains(attr.Val, "/eprint/") { ppr.URL = "https://eprint.iacr.org" + attr.Val // Extract ID from URL - parts := strings.Split(attr.Val, "/") + parts := core.Split(attr.Val, "/") if len(parts) >= 2 { ppr.ID = parts[len(parts)-2] + "-" + parts[len(parts)-1] } } } if ppr.Title == "" { - ppr.Title = strings.TrimSpace(extractText(n)) + ppr.Title = core.Trim(extractText(n)) } case "span": for _, attr := range n.Attr { if attr.Key == "class" { switch { - case strings.Contains(attr.Val, "author"): - author := strings.TrimSpace(extractText(n)) + case core.Contains(attr.Val, "author"): + author := core.Trim(extractText(n)) if author != "" { ppr.Authors = append(ppr.Authors, author) } - case strings.Contains(attr.Val, "date"): - ppr.Date = strings.TrimSpace(extractText(n)) + case core.Contains(attr.Val, "date"): + ppr.Date = core.Trim(extractText(n)) } } } case "p": for _, attr := range n.Attr { - if attr.Key == "class" && strings.Contains(attr.Val, "abstract") { - ppr.Abstract = strings.TrimSpace(extractText(n)) + if attr.Key == "class" && core.Contains(attr.Val, "abstract") { + ppr.Abstract = core.Trim(extractText(n)) } } } @@ -380,23 +379,23 @@ func parseIACREntry(node *html.Node) paper { // formatPaperMarkdown formats a paper as markdown. func formatPaperMarkdown(ppr paper) string { var b strings.Builder - fmt.Fprintf(&b, "# %s\n\n", ppr.Title) + b.WriteString(core.Sprintf("# %s\n\n", ppr.Title)) if len(ppr.Authors) > 0 { - fmt.Fprintf(&b, "- **Authors:** %s\n", strings.Join(ppr.Authors, ", ")) + b.WriteString(core.Sprintf("- **Authors:** %s\n", core.Join(", ", ppr.Authors...))) } if ppr.Date != "" { - fmt.Fprintf(&b, "- **Published:** %s\n", ppr.Date) + b.WriteString(core.Sprintf("- **Published:** %s\n", ppr.Date)) } if ppr.URL != "" { - fmt.Fprintf(&b, "- **URL:** %s\n", ppr.URL) + b.WriteString(core.Sprintf("- **URL:** %s\n", ppr.URL)) } if ppr.Source != "" { - fmt.Fprintf(&b, "- **Source:** %s\n", ppr.Source) + b.WriteString(core.Sprintf("- **Source:** %s\n", ppr.Source)) } if ppr.Abstract != "" { - fmt.Fprintf(&b, "\n## Abstract\n\n%s\n", ppr.Abstract) + b.WriteString(core.Sprintf("\n## Abstract\n\n%s\n", ppr.Abstract)) } return b.String() diff --git a/collect/papers_http_test.go b/collect/papers_http_test.go index b755413..f3ab1d1 100644 --- a/collect/papers_http_test.go +++ b/collect/papers_http_test.go @@ -4,9 +4,9 @@ import ( "context" "net/http" "net/http/httptest" - "strings" "testing" + core "dappco.re/go/core" "dappco.re/go/core/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -279,7 +279,7 @@ func TestPapersCollector_CollectAll_Good_OneFails(t *testing.T) { } func TestExtractIACRPapers_Good(t *testing.T) { - doc, err := html.Parse(strings.NewReader(sampleIACRHTML)) + doc, err := html.Parse(core.NewReader(sampleIACRHTML)) require.NoError(t, err) papers := extractIACRPapers(doc) @@ -296,7 +296,7 @@ func TestExtractIACRPapers_Good(t *testing.T) { } func TestExtractIACRPapers_Good_Empty(t *testing.T) { - doc, err := html.Parse(strings.NewReader(``)) + doc, err := html.Parse(core.NewReader(``)) require.NoError(t, err) papers := extractIACRPapers(doc) @@ -304,7 +304,7 @@ func TestExtractIACRPapers_Good_Empty(t *testing.T) { } func TestExtractIACRPapers_Good_NoTitle(t *testing.T) { - doc, err := html.Parse(strings.NewReader(`
`)) + doc, err := html.Parse(core.NewReader(`
`)) require.NoError(t, err) papers := extractIACRPapers(doc) diff --git a/collect/process.go b/collect/process.go index c0fb8d2..fc51c8e 100644 --- a/collect/process.go +++ b/collect/process.go @@ -2,14 +2,14 @@ package collect import ( "context" - "encoding/json" - "fmt" "maps" - "path/filepath" "slices" "strings" - core "dappco.re/go/core/log" + "encoding/json" + + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" "golang.org/x/net/html" ) @@ -24,7 +24,7 @@ type Processor struct { // Name returns the processor name. func (p *Processor) Name() string { - return fmt.Sprintf("process:%s", p.Source) + return core.Sprintf("process:%s", p.Source) } // Process reads files from the source directory, converts HTML or JSON @@ -33,33 +33,33 @@ func (p *Processor) Process(ctx context.Context, cfg *Config) (*Result, error) { result := &Result{Source: p.Name()} if p.Dir == "" { - return result, core.E("collect.Processor.Process", "directory is required", nil) + return result, coreerr.E("collect.Processor.Process", "directory is required", nil) } if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitStart(p.Name(), fmt.Sprintf("Processing files in %s", p.Dir)) + cfg.Dispatcher.EmitStart(p.Name(), core.Sprintf("Processing files in %s", p.Dir)) } if cfg.DryRun { if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitProgress(p.Name(), fmt.Sprintf("[dry-run] Would process files in %s", p.Dir), nil) + cfg.Dispatcher.EmitProgress(p.Name(), core.Sprintf("[dry-run] Would process files in %s", p.Dir), nil) } return result, nil } entries, err := cfg.Output.List(p.Dir) if err != nil { - return result, core.E("collect.Processor.Process", "failed to list directory", err) + return result, coreerr.E("collect.Processor.Process", "failed to list directory", err) } - outputDir := filepath.Join(cfg.OutputDir, "processed", p.Source) + outputDir := core.JoinPath(cfg.OutputDir, "processed", p.Source) if err := cfg.Output.EnsureDir(outputDir); err != nil { - return result, core.E("collect.Processor.Process", "failed to create output directory", err) + return result, coreerr.E("collect.Processor.Process", "failed to create output directory", err) } for _, entry := range entries { if ctx.Err() != nil { - return result, core.E("collect.Processor.Process", "context cancelled", ctx.Err()) + return result, coreerr.E("collect.Processor.Process", "context cancelled", ctx.Err()) } if entry.IsDir() { @@ -67,7 +67,7 @@ func (p *Processor) Process(ctx context.Context, cfg *Config) (*Result, error) { } name := entry.Name() - srcPath := filepath.Join(p.Dir, name) + srcPath := core.JoinPath(p.Dir, name) content, err := cfg.Output.Read(srcPath) if err != nil { @@ -76,7 +76,7 @@ func (p *Processor) Process(ctx context.Context, cfg *Config) (*Result, error) { } var processed string - ext := strings.ToLower(filepath.Ext(name)) + ext := core.Lower(core.PathExt(name)) switch ext { case ".html", ".htm": @@ -84,7 +84,7 @@ func (p *Processor) Process(ctx context.Context, cfg *Config) (*Result, error) { if err != nil { result.Errors++ if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitError(p.Name(), fmt.Sprintf("Failed to convert %s: %v", name, err), nil) + cfg.Dispatcher.EmitError(p.Name(), core.Sprintf("Failed to convert %s: %v", name, err), nil) } continue } @@ -93,21 +93,21 @@ func (p *Processor) Process(ctx context.Context, cfg *Config) (*Result, error) { if err != nil { result.Errors++ if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitError(p.Name(), fmt.Sprintf("Failed to convert %s: %v", name, err), nil) + cfg.Dispatcher.EmitError(p.Name(), core.Sprintf("Failed to convert %s: %v", name, err), nil) } continue } case ".md": // Already markdown, just clean up - processed = strings.TrimSpace(content) + processed = core.Trim(content) default: result.Skipped++ continue } // Write with .md extension - outName := strings.TrimSuffix(name, ext) + ".md" - outPath := filepath.Join(outputDir, outName) + outName := core.TrimSuffix(name, ext) + ".md" + outPath := core.JoinPath(outputDir, outName) if err := cfg.Output.Write(outPath, processed); err != nil { result.Errors++ @@ -118,12 +118,12 @@ func (p *Processor) Process(ctx context.Context, cfg *Config) (*Result, error) { result.Files = append(result.Files, outPath) if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitItem(p.Name(), fmt.Sprintf("Processed: %s", name), nil) + cfg.Dispatcher.EmitItem(p.Name(), core.Sprintf("Processed: %s", name), nil) } } if cfg.Dispatcher != nil { - cfg.Dispatcher.EmitComplete(p.Name(), fmt.Sprintf("Processed %d files", result.Items), result) + cfg.Dispatcher.EmitComplete(p.Name(), core.Sprintf("Processed %d files", result.Items), result) } return result, nil @@ -131,14 +131,14 @@ func (p *Processor) Process(ctx context.Context, cfg *Config) (*Result, error) { // htmlToMarkdown converts HTML content to clean markdown. func htmlToMarkdown(content string) (string, error) { - doc, err := html.Parse(strings.NewReader(content)) + doc, err := html.Parse(core.NewReader(content)) if err != nil { - return "", core.E("collect.htmlToMarkdown", "failed to parse HTML", err) + return "", coreerr.E("collect.htmlToMarkdown", "failed to parse HTML", err) } var b strings.Builder nodeToMarkdown(&b, doc, 0) - return strings.TrimSpace(b.String()), nil + return core.Trim(b.String()), nil } // nodeToMarkdown recursively converts an HTML node tree to markdown. @@ -146,7 +146,7 @@ func nodeToMarkdown(b *strings.Builder, n *html.Node, depth int) { switch n.Type { case html.TextNode: text := n.Data - if strings.TrimSpace(text) != "" { + if core.Trim(text) != "" { b.WriteString(text) } case html.ElementNode: @@ -220,7 +220,7 @@ func nodeToMarkdown(b *strings.Builder, n *html.Node, depth int) { } text := getChildrenText(n) if href != "" { - fmt.Fprintf(b, "[%s](%s)", text, href) + b.WriteString(core.Sprintf("[%s](%s)", text, href)) } else { b.WriteString(text) } @@ -232,7 +232,7 @@ func nodeToMarkdown(b *strings.Builder, n *html.Node, depth int) { counter := 1 for c := n.FirstChild; c != nil; c = c.NextSibling { if c.Type == html.ElementNode && c.Data == "li" { - fmt.Fprintf(b, "%d. ", counter) + b.WriteString(core.Sprintf("%d. ", counter)) for gc := c.FirstChild; gc != nil; gc = gc.NextSibling { nodeToMarkdown(b, gc, depth+1) } @@ -251,7 +251,7 @@ func nodeToMarkdown(b *strings.Builder, n *html.Node, depth int) { case "blockquote": b.WriteString("\n> ") text := getChildrenText(n) - b.WriteString(strings.ReplaceAll(text, "\n", "\n> ")) + b.WriteString(core.Replace(text, "\n", "\n> ")) b.WriteString("\n") return case "hr": @@ -289,13 +289,13 @@ func getChildrenText(n *html.Node) string { func jsonToMarkdown(content string) (string, error) { var data any if err := json.Unmarshal([]byte(content), &data); err != nil { - return "", core.E("collect.jsonToMarkdown", "failed to parse JSON", err) + return "", coreerr.E("collect.jsonToMarkdown", "failed to parse JSON", err) } var b strings.Builder b.WriteString("# Data\n\n") jsonValueToMarkdown(&b, data, 0) - return strings.TrimSpace(b.String()), nil + return core.Trim(b.String()), nil } // jsonValueToMarkdown recursively formats a JSON value as markdown. @@ -307,10 +307,10 @@ func jsonValueToMarkdown(b *strings.Builder, data any, depth int) { indent := strings.Repeat(" ", depth) switch child := val.(type) { case map[string]any, []any: - fmt.Fprintf(b, "%s- **%s:**\n", indent, key) + b.WriteString(core.Sprintf("%s- **%s:**\n", indent, key)) jsonValueToMarkdown(b, child, depth+1) default: - fmt.Fprintf(b, "%s- **%s:** %v\n", indent, key, val) + b.WriteString(core.Sprintf("%s- **%s:** %v\n", indent, key, val)) } } case []any: @@ -318,15 +318,15 @@ func jsonValueToMarkdown(b *strings.Builder, data any, depth int) { indent := strings.Repeat(" ", depth) switch child := item.(type) { case map[string]any, []any: - fmt.Fprintf(b, "%s- Item %d:\n", indent, i+1) + b.WriteString(core.Sprintf("%s- Item %d:\n", indent, i+1)) jsonValueToMarkdown(b, child, depth+1) default: - fmt.Fprintf(b, "%s- %v\n", indent, item) + b.WriteString(core.Sprintf("%s- %v\n", indent, item)) } } default: indent := strings.Repeat(" ", depth) - fmt.Fprintf(b, "%s%v\n", indent, data) + b.WriteString(core.Sprintf("%s%v\n", indent, data)) } } diff --git a/collect/ratelimit.go b/collect/ratelimit.go index 5fc4969..e5c384a 100644 --- a/collect/ratelimit.go +++ b/collect/ratelimit.go @@ -2,7 +2,6 @@ package collect import ( "context" - "fmt" "maps" "os/exec" "strconv" @@ -10,7 +9,8 @@ import ( "sync" "time" - core "dappco.re/go/core/log" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" ) // RateLimiter tracks per-source rate limiting to avoid overwhelming APIs. @@ -63,7 +63,7 @@ func (r *RateLimiter) Wait(ctx context.Context, source string) error { // Wait outside the lock, then reclaim. select { case <-ctx.Done(): - return core.E("collect.RateLimiter.Wait", "context cancelled", ctx.Err()) + return coreerr.E("collect.RateLimiter.Wait", "context cancelled", ctx.Err()) case <-time.After(remaining): } @@ -106,23 +106,23 @@ func (r *RateLimiter) CheckGitHubRateLimitCtx(ctx context.Context) (used, limit cmd := exec.CommandContext(ctx, "gh", "api", "rate_limit", "--jq", ".rate | \"\\(.used) \\(.limit)\"") out, err := cmd.Output() if err != nil { - return 0, 0, core.E("collect.RateLimiter.CheckGitHubRateLimit", "failed to check rate limit", err) + return 0, 0, coreerr.E("collect.RateLimiter.CheckGitHubRateLimit", "failed to check rate limit", err) } - parts := strings.Fields(strings.TrimSpace(string(out))) + parts := strings.Fields(core.Trim(string(out))) if len(parts) != 2 { - return 0, 0, core.E("collect.RateLimiter.CheckGitHubRateLimit", - fmt.Sprintf("unexpected output format: %q", string(out)), nil) + return 0, 0, coreerr.E("collect.RateLimiter.CheckGitHubRateLimit", + core.Sprintf("unexpected output format: %q", string(out)), nil) } used, err = strconv.Atoi(parts[0]) if err != nil { - return 0, 0, core.E("collect.RateLimiter.CheckGitHubRateLimit", "failed to parse used count", err) + return 0, 0, coreerr.E("collect.RateLimiter.CheckGitHubRateLimit", "failed to parse used count", err) } limit, err = strconv.Atoi(parts[1]) if err != nil { - return 0, 0, core.E("collect.RateLimiter.CheckGitHubRateLimit", "failed to parse limit count", err) + return 0, 0, coreerr.E("collect.RateLimiter.CheckGitHubRateLimit", "failed to parse limit count", err) } // Auto-pause at 75% usage diff --git a/collect/state.go b/collect/state.go index 08e2b95..1f66177 100644 --- a/collect/state.go +++ b/collect/state.go @@ -5,7 +5,7 @@ import ( "sync" "time" - core "dappco.re/go/core/log" + coreerr "dappco.re/go/core/log" "dappco.re/go/core/io" ) @@ -59,12 +59,12 @@ func (s *State) Load() error { data, err := s.medium.Read(s.path) if err != nil { - return core.E("collect.State.Load", "failed to read state file", err) + return coreerr.E("collect.State.Load", "failed to read state file", err) } var entries map[string]*StateEntry if err := json.Unmarshal([]byte(data), &entries); err != nil { - return core.E("collect.State.Load", "failed to parse state file", err) + return coreerr.E("collect.State.Load", "failed to parse state file", err) } if entries == nil { @@ -81,11 +81,11 @@ func (s *State) Save() error { data, err := json.MarshalIndent(s.entries, "", " ") if err != nil { - return core.E("collect.State.Save", "failed to marshal state", err) + return coreerr.E("collect.State.Save", "failed to marshal state", err) } if err := s.medium.Write(s.path, string(data)); err != nil { - return core.E("collect.State.Save", "failed to write state file", err) + return coreerr.E("collect.State.Save", "failed to write state file", err) } return nil diff --git a/forge/client_test.go b/forge/client_test.go index daf05c8..7f5f5ed 100644 --- a/forge/client_test.go +++ b/forge/client_test.go @@ -2,11 +2,12 @@ package forge import ( "encoding/json" - "fmt" "net/http" "net/http/httptest" "testing" + core "dappco.re/go/core" + forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2" "github.com/stretchr/testify/assert" @@ -459,7 +460,7 @@ func TestListPullRequests_StateMapping(t *testing.T) { }) var capturedState string - mux.HandleFunc(fmt.Sprintf("/api/v1/repos/owner/repo/pulls"), func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc(core.Sprintf("/api/v1/repos/owner/repo/pulls"), func(w http.ResponseWriter, r *http.Request) { capturedState = r.URL.Query().Get("state") w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode([]any{}) diff --git a/forge/prs.go b/forge/prs.go index d8d92f7..4472c32 100644 --- a/forge/prs.go +++ b/forge/prs.go @@ -2,12 +2,13 @@ package forge import ( "bytes" - "encoding/json" - "fmt" "net/http" "net/url" "strconv" + "encoding/json" + + core "dappco.re/go/core" "dappco.re/go/core/log" "dappco.re/go/core/scm/agentci" @@ -32,7 +33,7 @@ func (c *Client) MergePullRequest(owner, repo string, index int64, method string return log.E("forge.MergePullRequest", "failed to merge pull request", err) } if !merged { - return log.E("forge.MergePullRequest", fmt.Sprintf("merge returned false for %s/%s#%d", owner, repo, index), nil) + return log.E("forge.MergePullRequest", core.Sprintf("merge returned false for %s/%s#%d", owner, repo, index), nil) } return nil } @@ -75,7 +76,7 @@ func (c *Client) SetPRDraft(owner, repo string, index int64, draft bool) error { defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return log.E("forge.SetPRDraft", fmt.Sprintf("unexpected status %d", resp.StatusCode), nil) + return log.E("forge.SetPRDraft", core.Sprintf("unexpected status %d", resp.StatusCode), nil) } return nil } diff --git a/forge/prs_test.go b/forge/prs_test.go index aabe584..a6b9f64 100644 --- a/forge/prs_test.go +++ b/forge/prs_test.go @@ -1,10 +1,10 @@ package forge import ( + core "dappco.re/go/core" "encoding/json" "net/http" "net/http/httptest" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -44,8 +44,8 @@ func TestClient_MergePullRequest_Bad_ServerError(t *testing.T) { // The error may be "failed to merge" or "merge returned false" depending on // how the error server responds. assert.True(t, - strings.Contains(err.Error(), "failed to merge") || - strings.Contains(err.Error(), "merge returned false"), + core.Contains(err.Error(), "failed to merge") || + core.Contains(err.Error(), "merge returned false"), "unexpected error: %s", err.Error()) } diff --git a/forge/testhelper_test.go b/forge/testhelper_test.go index e38db64..563c831 100644 --- a/forge/testhelper_test.go +++ b/forge/testhelper_test.go @@ -1,10 +1,10 @@ package forge import ( + core "dappco.re/go/core" "encoding/json" "net/http" "net/http/httptest" - "strings" "testing" ) @@ -296,7 +296,7 @@ func newForgejoMux() *http.ServeMux { // Generic fallback — handles PATCH for SetPRDraft and other unmatched routes. mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // Handle PATCH requests (SetPRDraft). - if r.Method == http.MethodPatch && strings.Contains(r.URL.Path, "/pulls/") { + if r.Method == http.MethodPatch && core.Contains(r.URL.Path, "/pulls/") { jsonResponse(w, map[string]any{ "number": 1, "title": "test PR", "state": "open", }) diff --git a/git/git.go b/git/git.go index 53ded5f..14093de 100644 --- a/git/git.go +++ b/git/git.go @@ -2,6 +2,7 @@ package git import ( + core "dappco.re/go/core" "bytes" "context" "io" @@ -96,7 +97,7 @@ func getStatus(ctx context.Context, path, name string) RepoStatus { status.Error = err return status } - status.Branch = strings.TrimSpace(branch) + status.Branch = core.Trim(branch) // Get porcelain status porcelain, err := gitCommand(ctx, path, "status", "--porcelain") @@ -142,13 +143,13 @@ func getAheadBehind(ctx context.Context, path string) (ahead, behind int) { // Try to get ahead count aheadStr, err := gitCommand(ctx, path, "rev-list", "--count", "@{u}..HEAD") if err == nil { - ahead, _ = strconv.Atoi(strings.TrimSpace(aheadStr)) + ahead, _ = strconv.Atoi(core.Trim(aheadStr)) } // Try to get behind count behindStr, err := gitCommand(ctx, path, "rev-list", "--count", "HEAD..@{u}") if err == nil { - behind, _ = strconv.Atoi(strings.TrimSpace(behindStr)) + behind, _ = strconv.Atoi(core.Trim(behindStr)) } return ahead, behind @@ -172,9 +173,9 @@ func IsNonFastForward(err error) bool { return false } msg := err.Error() - return strings.Contains(msg, "non-fast-forward") || - strings.Contains(msg, "fetch first") || - strings.Contains(msg, "tip of your current branch is behind") + return core.Contains(msg, "non-fast-forward") || + core.Contains(msg, "fetch first") || + core.Contains(msg, "tip of your current branch is behind") } // gitInteractive runs a git command with terminal attached for user interaction. @@ -271,7 +272,7 @@ type GitError struct { // Error returns the git error message, preferring stderr output. func (e *GitError) Error() string { // Return just the stderr message, trimmed - msg := strings.TrimSpace(e.Stderr) + msg := core.Trim(e.Stderr) if msg != "" { return msg } diff --git a/git/service.go b/git/service.go index 13d66c6..f74c93f 100644 --- a/git/service.go +++ b/git/service.go @@ -62,11 +62,36 @@ func NewService(opts ServiceOptions) func(*core.Core) (any, error) { } } -// OnStartup registers query and task handlers. -func (s *Service) OnStartup(ctx context.Context) error { +// OnStartup registers query and action handlers. +func (s *Service) OnStartup(ctx context.Context) core.Result { s.Core().RegisterQuery(s.handleQuery) - s.Core().RegisterTask(s.handleTask) - return nil + + s.Core().Action("git.push", func(ctx context.Context, opts core.Options) core.Result { + path := opts.String("path") + if err := Push(ctx, path); err != nil { + return core.Result{Value: err} + } + return core.Result{OK: true} + }) + + s.Core().Action("git.pull", func(ctx context.Context, opts core.Options) core.Result { + path := opts.String("path") + if err := Pull(ctx, path); err != nil { + return core.Result{Value: err} + } + return core.Result{OK: true} + }) + + s.Core().Action("git.push-multiple", func(ctx context.Context, opts core.Options) core.Result { + r := opts.Get("paths") + paths, _ := r.Value.([]string) + r = opts.Get("names") + names, _ := r.Value.(map[string]string) + results := PushMultiple(ctx, paths, names) + return core.Result{Value: results, OK: true} + }) + + return core.Result{OK: true} } func (s *Service) handleQuery(c *core.Core, q core.Query) core.Result { @@ -85,21 +110,6 @@ func (s *Service) handleQuery(c *core.Core, q core.Query) core.Result { return core.Result{} } -func (s *Service) handleTask(c *core.Core, t core.Task) core.Result { - switch m := t.(type) { - case TaskPush: - return core.Result{}.Result(nil, Push(context.Background(), m.Path)) - - case TaskPull: - return core.Result{}.Result(nil, Pull(context.Background(), m.Path)) - - case TaskPushMultiple: - results := PushMultiple(context.Background(), m.Paths, m.Names) - return core.Result{Value: results, OK: true} - } - return core.Result{} -} - // Status returns last status result. func (s *Service) Status() []RepoStatus { return s.lastStatus } diff --git a/gitea/testhelper_test.go b/gitea/testhelper_test.go index daea37b..376b09f 100644 --- a/gitea/testhelper_test.go +++ b/gitea/testhelper_test.go @@ -1,10 +1,10 @@ package gitea import ( + core "dappco.re/go/core" "encoding/json" "net/http" "net/http/httptest" - "strings" "testing" ) @@ -137,7 +137,7 @@ func newGiteaMux() *http.ServeMux { // Fallback for PATCH requests and unmatched routes. mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodPatch && strings.Contains(r.URL.Path, "/pulls/") { + if r.Method == http.MethodPatch && core.Contains(r.URL.Path, "/pulls/") { jsonResponse(w, map[string]any{ "number": 1, "title": "test PR", "state": "open", }) diff --git a/go.mod b/go.mod index 2ffcf2b..81e10c6 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.26.0 require ( code.gitea.io/sdk/gitea v0.23.2 codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 - dappco.re/go/core v0.5.0 + dappco.re/go/core v0.8.0-alpha.1 dappco.re/go/core/api v0.2.0 dappco.re/go/core/i18n v0.2.0 dappco.re/go/core/io v0.2.0 diff --git a/go.sum b/go.sum index 0e4dab9..cb936b4 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg= code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 h1:HTCWpzyWQOHDWt3LzI6/d2jvUDsw/vgGRWm/8BTvcqI= codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0/go.mod h1:ZglEEDj+qkxYUb+SQIeqGtFxQrbaMYqIOgahNKb7uxs= -dappco.re/go/core v0.5.0 h1:P5DJoaCiK5Q+af5UiTdWqUIW4W4qYKzpgGK50thm21U= -dappco.re/go/core v0.5.0/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= +dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk= +dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= dappco.re/go/core/api v0.2.0 h1:5OcN9nawpp18Jp6dB1OwI2CBfs0Tacb0y0zqxFB6TJ0= dappco.re/go/core/api v0.2.0/go.mod h1:AtgNAx8lDY+qhVObFdNQOjSUQrHX1BeiDdMuA6RIfzo= dappco.re/go/core/i18n v0.2.0 h1:NHzk6RCU93/qVRA3f2jvMr9P1R6FYheR/sHL+TnvKbI= diff --git a/jobrunner/forgejo/source.go b/jobrunner/forgejo/source.go index 61f8970..918705e 100644 --- a/jobrunner/forgejo/source.go +++ b/jobrunner/forgejo/source.go @@ -2,11 +2,10 @@ package forgejo import ( "context" - "fmt" - "strings" "dappco.re/go/core/scm/forge" "dappco.re/go/core/scm/jobrunner" + core "dappco.re/go/core" "dappco.re/go/core/log" ) @@ -69,9 +68,9 @@ func (s *ForgejoSource) Report(ctx context.Context, result *jobrunner.ActionResu status = "failed" } - body := fmt.Sprintf("**jobrunner** `%s` %s for #%d (PR #%d)", result.Action, status, result.ChildNumber, result.PRNumber) + body := core.Sprintf("**jobrunner** `%s` %s for #%d (PR #%d)", result.Action, status, result.ChildNumber, result.PRNumber) if result.Error != "" { - body += fmt.Sprintf("\n\n```\n%s\n```", result.Error) + body += core.Sprintf("\n\n```\n%s\n```", result.Error) } return s.forge.CreateIssueComment(result.RepoOwner, result.RepoName, int64(result.EpicNumber), body) @@ -165,9 +164,9 @@ type epicInfo struct { // splitRepo parses "owner/repo" into its components. func splitRepo(full string) (string, string, error) { - parts := strings.SplitN(full, "/", 2) + parts := core.SplitN(full, "/", 2) if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return "", "", log.E("forgejo.splitRepo", fmt.Sprintf("expected owner/repo format, got %q", full), nil) + return "", "", log.E("forgejo.splitRepo", core.Sprintf("expected owner/repo format, got %q", full), nil) } return parts[0], parts[1], nil } diff --git a/jobrunner/forgejo/source_test.go b/jobrunner/forgejo/source_test.go index 965e765..3c89e51 100644 --- a/jobrunner/forgejo/source_test.go +++ b/jobrunner/forgejo/source_test.go @@ -5,12 +5,12 @@ import ( "encoding/json" "net/http" "net/http/httptest" - "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + core "dappco.re/go/core" "dappco.re/go/core/scm/forge" "dappco.re/go/core/scm/jobrunner" ) @@ -19,7 +19,7 @@ import ( // endpoint that the SDK calls during NewClient initialization. func withVersion(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.HasSuffix(r.URL.Path, "/version") { + if core.HasSuffix(r.URL.Path, "/version") { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"version":"9.0.0"}`)) return @@ -47,7 +47,7 @@ func TestForgejoSource_Poll_Good(t *testing.T) { switch { // List issues — return one epic - case strings.Contains(path, "/issues"): + case core.Contains(path, "/issues"): issues := []map[string]any{ { "number": 10, @@ -59,7 +59,7 @@ func TestForgejoSource_Poll_Good(t *testing.T) { _ = json.NewEncoder(w).Encode(issues) // List PRs — return one open PR linked to #11 - case strings.Contains(path, "/pulls"): + case core.Contains(path, "/pulls"): prs := []map[string]any{ { "number": 20, @@ -73,7 +73,7 @@ func TestForgejoSource_Poll_Good(t *testing.T) { _ = json.NewEncoder(w).Encode(prs) // Combined status - case strings.Contains(path, "/status"): + case core.Contains(path, "/status"): status := map[string]any{ "state": "success", "total_count": 1, diff --git a/jobrunner/handlers/completion.go b/jobrunner/handlers/completion.go index 0c9b40e..87848b5 100644 --- a/jobrunner/handlers/completion.go +++ b/jobrunner/handlers/completion.go @@ -2,9 +2,9 @@ package handlers import ( "context" - "fmt" "time" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "dappco.re/go/core/scm/forge" "dappco.re/go/core/scm/jobrunner" @@ -70,7 +70,7 @@ func (h *CompletionHandler) Execute(ctx context.Context, signal *jobrunner.Pipel msg := "Agent reported failure." if signal.Error != "" { - msg += fmt.Sprintf("\n\nError: %s", signal.Error) + msg += core.Sprintf("\n\nError: %s", signal.Error) } _ = h.forge.CreateIssueComment(signal.RepoOwner, signal.RepoName, int64(signal.ChildNumber), msg) } diff --git a/jobrunner/handlers/dispatch.go b/jobrunner/handlers/dispatch.go index 961a9d9..2b29402 100644 --- a/jobrunner/handlers/dispatch.go +++ b/jobrunner/handlers/dispatch.go @@ -4,11 +4,10 @@ import ( "bytes" "context" "encoding/json" - "fmt" "path" - "strings" "time" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "dappco.re/go/core/scm/agentci" "dappco.re/go/core/scm/forge" @@ -146,7 +145,7 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin // Build ticket. targetBranch := "new" // TODO: resolve from epic or repo default - ticketID := fmt.Sprintf("%s-%s-%d-%d", safeOwner, safeRepo, signal.ChildNumber, time.Now().Unix()) + ticketID := core.Sprintf("%s-%s-%d-%d", safeOwner, safeRepo, signal.ChildNumber, time.Now().Unix()) ticket := DispatchTicket{ ID: ticketID, @@ -173,7 +172,7 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin } // Check if ticket already exists on agent (dedup). - ticketName := fmt.Sprintf("ticket-%s-%s-%d.json", safeOwner, safeRepo, signal.ChildNumber) + ticketName := core.Sprintf("ticket-%s-%s-%d.json", safeOwner, safeRepo, signal.ChildNumber) if h.ticketExists(ctx, agent, ticketName) { coreerr.Info("ticket already queued, skipping", "ticket", ticketName, "agent", signal.Assignee) return &jobrunner.ActionResult{ @@ -194,7 +193,7 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin return nil, coreerr.E("dispatch.Execute", "ticket path", err) } if err := h.secureTransfer(ctx, agent, remoteTicketPath, ticketJSON, 0644); err != nil { - h.failDispatch(signal, fmt.Sprintf("Ticket transfer failed: %v", err)) + h.failDispatch(signal, core.Sprintf("Ticket transfer failed: %v", err)) return &jobrunner.ActionResult{ Action: "dispatch", RepoOwner: safeOwner, @@ -202,22 +201,22 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin EpicNumber: signal.EpicNumber, ChildNumber: signal.ChildNumber, Success: false, - Error: fmt.Sprintf("transfer ticket: %v", err), + Error: core.Sprintf("transfer ticket: %v", err), Timestamp: time.Now(), Duration: time.Since(start), }, nil } // Transfer token via separate .env file with 0600 permissions. - envContent := fmt.Sprintf("FORGE_TOKEN=%s\n", h.token) - remoteEnvPath, err := agentci.JoinRemotePath(queueDir, fmt.Sprintf(".env.%s", ticketID)) + envContent := core.Sprintf("FORGE_TOKEN=%s\n", h.token) + remoteEnvPath, err := agentci.JoinRemotePath(queueDir, core.Sprintf(".env.%s", ticketID)) if err != nil { return nil, coreerr.E("dispatch.Execute", "env path", err) } if err := h.secureTransfer(ctx, agent, remoteEnvPath, []byte(envContent), 0600); err != nil { // Clean up the ticket if env transfer fails. _ = h.runRemote(ctx, agent, "rm", "-f", remoteTicketPath) - h.failDispatch(signal, fmt.Sprintf("Token transfer failed: %v", err)) + h.failDispatch(signal, core.Sprintf("Token transfer failed: %v", err)) return &jobrunner.ActionResult{ Action: "dispatch", RepoOwner: safeOwner, @@ -225,7 +224,7 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin EpicNumber: signal.EpicNumber, ChildNumber: signal.ChildNumber, Success: false, - Error: fmt.Sprintf("transfer token: %v", err), + Error: core.Sprintf("transfer token: %v", err), Timestamp: time.Now(), Duration: time.Since(start), }, nil @@ -236,7 +235,7 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin if runMode == agentci.ModeDual { modeStr = "Clotho Verified (Dual Run)" } - comment := fmt.Sprintf("Dispatched to **%s** agent queue.\nMode: **%s**", signal.Assignee, modeStr) + comment := core.Sprintf("Dispatched to **%s** agent queue.\nMode: **%s**", signal.Assignee, modeStr) _ = h.forge.CreateIssueComment(safeOwner, safeRepo, int64(signal.ChildNumber), comment) return &jobrunner.ActionResult{ @@ -261,20 +260,20 @@ func (h *DispatchHandler) failDispatch(signal *jobrunner.PipelineSignal, reason _ = h.forge.RemoveIssueLabel(signal.RepoOwner, signal.RepoName, int64(signal.ChildNumber), inProgressLabel.ID) } - _ = h.forge.CreateIssueComment(signal.RepoOwner, signal.RepoName, int64(signal.ChildNumber), fmt.Sprintf("Agent dispatch failed: %s", reason)) + _ = h.forge.CreateIssueComment(signal.RepoOwner, signal.RepoName, int64(signal.ChildNumber), core.Sprintf("Agent dispatch failed: %s", reason)) } // secureTransfer writes data to a remote path via SSH stdin, preventing command injection. func (h *DispatchHandler) secureTransfer(ctx context.Context, agent agentci.AgentConfig, remotePath string, data []byte, mode int) error { safePath := agentci.EscapeShellArg(remotePath) - remoteCmd := fmt.Sprintf("cat > %s && chmod %o %s", safePath, mode, safePath) + remoteCmd := core.Sprintf("cat > %s && chmod %o %s", safePath, mode, safePath) cmd := agentci.SecureSSHCommand(agent.Host, remoteCmd) cmd.Stdin = bytes.NewReader(data) output, err := cmd.CombinedOutput() if err != nil { - return coreerr.E("dispatch.transfer", fmt.Sprintf("ssh to %s failed: %s", agent.Host, string(output)), err) + return coreerr.E("dispatch.transfer", core.Sprintf("ssh to %s failed: %s", agent.Host, string(output)), err) } return nil } @@ -288,7 +287,7 @@ func (h *DispatchHandler) runRemote(ctx context.Context, agent agentci.AgentConf for _, arg := range args { escaped = append(escaped, agentci.EscapeShellArg(arg)) } - remoteCmd = strings.Join(escaped, " ") + remoteCmd = core.Join(" ", escaped...) } cmd := agentci.SecureSSHCommand(agent.Host, remoteCmd) @@ -326,7 +325,7 @@ func (h *DispatchHandler) ticketExists(ctx context.Context, agent agentci.AgentC queuePath = agentci.EscapeShellArg(queuePath) activePath = agentci.EscapeShellArg(activePath) donePath = agentci.EscapeShellArg(donePath) - checkCmd := fmt.Sprintf( + checkCmd := core.Sprintf( "test -f %s || test -f %s || test -f %s", queuePath, activePath, donePath, ) diff --git a/jobrunner/handlers/dispatch_test.go b/jobrunner/handlers/dispatch_test.go index 0f733b3..d413fd7 100644 --- a/jobrunner/handlers/dispatch_test.go +++ b/jobrunner/handlers/dispatch_test.go @@ -6,10 +6,10 @@ import ( "net/http" "net/http/httptest" "os" - "path/filepath" "strconv" "testing" + core "dappco.re/go/core" "dappco.re/go/core/scm/agentci" "dappco.re/go/core/scm/jobrunner" "github.com/stretchr/testify/assert" @@ -19,7 +19,7 @@ import ( func writeFakeSSHCommand(t *testing.T, outputPath string) string { t.Helper() dir := t.TempDir() - script := filepath.Join(dir, "ssh") + script := core.JoinPath(dir, "ssh") scriptContent := "#!/bin/sh\n" + "OUT=" + strconv.Quote(outputPath) + "\n" + "printf '%s\n' \"$@\" >> \"$OUT\"\n" + @@ -253,7 +253,7 @@ func TestDispatch_TicketJSON_Good_OmitsEmptyModelRunner(t *testing.T) { } func TestDispatch_runRemote_Good_EscapesPath(t *testing.T) { - outputPath := filepath.Join(t.TempDir(), "ssh-output.txt") + outputPath := core.JoinPath(t.TempDir(), "ssh-output.txt") toolPath := writeFakeSSHCommand(t, outputPath) t.Setenv("PATH", toolPath+":"+os.Getenv("PATH")) @@ -274,7 +274,7 @@ func TestDispatch_runRemote_Good_EscapesPath(t *testing.T) { } func TestDispatch_secureTransfer_Good_EscapesPath(t *testing.T) { - outputPath := filepath.Join(t.TempDir(), "ssh-output.txt") + outputPath := core.JoinPath(t.TempDir(), "ssh-output.txt") toolPath := writeFakeSSHCommand(t, outputPath) t.Setenv("PATH", toolPath+":"+os.Getenv("PATH")) diff --git a/jobrunner/handlers/enable_auto_merge.go b/jobrunner/handlers/enable_auto_merge.go index 7ab4d30..f507c4d 100644 --- a/jobrunner/handlers/enable_auto_merge.go +++ b/jobrunner/handlers/enable_auto_merge.go @@ -2,9 +2,9 @@ package handlers import ( "context" - "fmt" "time" + core "dappco.re/go/core" "dappco.re/go/core/scm/forge" "dappco.re/go/core/scm/jobrunner" ) @@ -51,7 +51,7 @@ func (h *EnableAutoMergeHandler) Execute(ctx context.Context, signal *jobrunner. } if err != nil { - result.Error = fmt.Sprintf("merge failed: %v", err) + result.Error = core.Sprintf("merge failed: %v", err) } return result, nil diff --git a/jobrunner/handlers/publish_draft.go b/jobrunner/handlers/publish_draft.go index 202726b..ae1c65e 100644 --- a/jobrunner/handlers/publish_draft.go +++ b/jobrunner/handlers/publish_draft.go @@ -2,9 +2,9 @@ package handlers import ( "context" - "fmt" "time" + core "dappco.re/go/core" "dappco.re/go/core/scm/forge" "dappco.re/go/core/scm/jobrunner" ) @@ -48,7 +48,7 @@ func (h *PublishDraftHandler) Execute(ctx context.Context, signal *jobrunner.Pip } if err != nil { - result.Error = fmt.Sprintf("publish draft failed: %v", err) + result.Error = core.Sprintf("publish draft failed: %v", err) } return result, nil diff --git a/jobrunner/handlers/resolve_threads.go b/jobrunner/handlers/resolve_threads.go index 19f8480..1c9aa73 100644 --- a/jobrunner/handlers/resolve_threads.go +++ b/jobrunner/handlers/resolve_threads.go @@ -2,11 +2,11 @@ package handlers import ( "context" - "fmt" "time" forgejosdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "dappco.re/go/core/scm/forge" "dappco.re/go/core/scm/jobrunner" @@ -72,7 +72,7 @@ func (h *DismissReviewsHandler) Execute(ctx context.Context, signal *jobrunner.P } if len(dismissErrors) > 0 { - result.Error = fmt.Sprintf("failed to dismiss %d review(s): %s", + result.Error = core.Sprintf("failed to dismiss %d review(s): %s", len(dismissErrors), dismissErrors[0]) } diff --git a/jobrunner/handlers/send_fix_command.go b/jobrunner/handlers/send_fix_command.go index 5b65eab..f87dd59 100644 --- a/jobrunner/handlers/send_fix_command.go +++ b/jobrunner/handlers/send_fix_command.go @@ -2,9 +2,9 @@ package handlers import ( "context" - "fmt" "time" + core "dappco.re/go/core" "dappco.re/go/core/scm/forge" "dappco.re/go/core/scm/jobrunner" ) @@ -67,7 +67,7 @@ func (h *SendFixCommandHandler) Execute(ctx context.Context, signal *jobrunner.P } if err != nil { - result.Error = fmt.Sprintf("post comment failed: %v", err) + result.Error = core.Sprintf("post comment failed: %v", err) } return result, nil diff --git a/jobrunner/handlers/testhelper_test.go b/jobrunner/handlers/testhelper_test.go index 20d966b..3a0ca13 100644 --- a/jobrunner/handlers/testhelper_test.go +++ b/jobrunner/handlers/testhelper_test.go @@ -2,11 +2,11 @@ package handlers import ( "net/http" - "strings" "testing" "github.com/stretchr/testify/require" + core "dappco.re/go/core" "dappco.re/go/core/scm/forge" ) @@ -17,7 +17,7 @@ const forgejoVersionResponse = `{"version":"9.0.0"}` // that the SDK calls during NewClient initialization. func withVersion(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.HasSuffix(r.URL.Path, "/version") { + if core.HasSuffix(r.URL.Path, "/version") { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(forgejoVersionResponse)) return diff --git a/jobrunner/handlers/tick_parent.go b/jobrunner/handlers/tick_parent.go index 2d4bb74..714cf63 100644 --- a/jobrunner/handlers/tick_parent.go +++ b/jobrunner/handlers/tick_parent.go @@ -2,12 +2,12 @@ package handlers import ( "context" - "fmt" "strings" "time" forgejosdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "dappco.re/go/core/scm/forge" "dappco.re/go/core/scm/jobrunner" @@ -46,10 +46,10 @@ func (h *TickParentHandler) Execute(ctx context.Context, signal *jobrunner.Pipel } oldBody := epic.Body - unchecked := fmt.Sprintf("- [ ] #%d", signal.ChildNumber) - checked := fmt.Sprintf("- [x] #%d", signal.ChildNumber) + unchecked := core.Sprintf("- [ ] #%d", signal.ChildNumber) + checked := core.Sprintf("- [x] #%d", signal.ChildNumber) - if !strings.Contains(oldBody, unchecked) { + if !core.Contains(oldBody, unchecked) { // Already ticked or not found -- nothing to do. return &jobrunner.ActionResult{ Action: "tick_parent", @@ -74,7 +74,7 @@ func (h *TickParentHandler) Execute(ctx context.Context, signal *jobrunner.Pipel RepoOwner: signal.RepoOwner, RepoName: signal.RepoName, PRNumber: signal.PRNumber, - Error: fmt.Sprintf("edit epic failed: %v", err), + Error: core.Sprintf("edit epic failed: %v", err), Timestamp: time.Now(), Duration: time.Since(start), }, nil @@ -94,7 +94,7 @@ func (h *TickParentHandler) Execute(ctx context.Context, signal *jobrunner.Pipel } if err != nil { - result.Error = fmt.Sprintf("close child issue failed: %v", err) + result.Error = core.Sprintf("close child issue failed: %v", err) } return result, nil diff --git a/jobrunner/handlers/tick_parent_test.go b/jobrunner/handlers/tick_parent_test.go index 836ecdf..95b5f12 100644 --- a/jobrunner/handlers/tick_parent_test.go +++ b/jobrunner/handlers/tick_parent_test.go @@ -6,12 +6,12 @@ import ( "io" "net/http" "net/http/httptest" - "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + core "dappco.re/go/core" "dappco.re/go/core/scm/jobrunner" ) @@ -43,7 +43,7 @@ func TestTickParent_Execute_Good(t *testing.T) { switch { // GET issue (fetch epic) - case method == http.MethodGet && strings.Contains(path, "/issues/42"): + case method == http.MethodGet && core.Contains(path, "/issues/42"): _ = json.NewEncoder(w).Encode(map[string]any{ "number": 42, "body": epicBody, @@ -51,7 +51,7 @@ func TestTickParent_Execute_Good(t *testing.T) { }) // PATCH issue (edit epic body) - case method == http.MethodPatch && strings.Contains(path, "/issues/42"): + case method == http.MethodPatch && core.Contains(path, "/issues/42"): b, _ := io.ReadAll(r.Body) editBody = string(b) _ = json.NewEncoder(w).Encode(map[string]any{ @@ -61,7 +61,7 @@ func TestTickParent_Execute_Good(t *testing.T) { }) // PATCH issue (close child — state: closed) - case method == http.MethodPatch && strings.Contains(path, "/issues/7"): + case method == http.MethodPatch && core.Contains(path, "/issues/7"): closeCalled = true _ = json.NewEncoder(w).Encode(map[string]any{ "number": 7, diff --git a/jobrunner/journal.go b/jobrunner/journal.go index 2e3976b..c16ebf7 100644 --- a/jobrunner/journal.go +++ b/jobrunner/journal.go @@ -1,13 +1,15 @@ package jobrunner import ( - "encoding/json" "os" "path/filepath" "regexp" "strings" "sync" + "encoding/json" + + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" coreio "dappco.re/go/core/io" ) @@ -64,7 +66,7 @@ func NewJournal(baseDir string) (*Journal, error) { // containing separators, and any value outside the safe character set. func sanitizePathComponent(name string) (string, error) { // Reject empty or whitespace-only values. - if name == "" || strings.TrimSpace(name) == "" { + if name == "" || core.Trim(name) == "" { return "", coreerr.E("jobrunner.sanitizePathComponent", "invalid path component: "+name, nil) } @@ -138,7 +140,7 @@ func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error { } date := result.Timestamp.UTC().Format("2006-01-02") - dir := filepath.Join(j.baseDir, owner, repo) + dir := core.JoinPath(j.baseDir, owner, repo) // Resolve to absolute path and verify it stays within baseDir. absBase, err := filepath.Abs(j.baseDir) @@ -149,7 +151,7 @@ func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error { if err != nil { return coreerr.E("jobrunner.Journal.Append", "resolve journal directory", err) } - if !strings.HasPrefix(absDir, absBase+string(filepath.Separator)) { + if !core.HasPrefix(absDir, absBase+string(filepath.Separator)) { return coreerr.E("jobrunner.Journal.Append", "journal path escapes base directory", nil) } @@ -160,7 +162,7 @@ func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error { return coreerr.E("jobrunner.Journal.Append", "create journal directory", err) } - path := filepath.Join(dir, date+".jsonl") + path := core.JoinPath(dir, date+".jsonl") f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return coreerr.E("jobrunner.Journal.Append", "open journal file", err) diff --git a/jobrunner/journal_test.go b/jobrunner/journal_test.go index a17a88b..ebc553f 100644 --- a/jobrunner/journal_test.go +++ b/jobrunner/journal_test.go @@ -1,11 +1,10 @@ package jobrunner import ( + core "dappco.re/go/core" "bufio" "encoding/json" "os" - "path/filepath" - "strings" "testing" "time" @@ -55,7 +54,7 @@ func TestJournal_Append_Good(t *testing.T) { require.NoError(t, err) // Read the file back. - expectedPath := filepath.Join(dir, "host-uk", "core-tenant", "2026-02-05.jsonl") + expectedPath := core.JoinPath(dir, "host-uk", "core-tenant", "2026-02-05.jsonl") f, err := os.Open(expectedPath) require.NoError(t, err) defer func() { _ = f.Close() }() @@ -106,7 +105,7 @@ func TestJournal_Append_Good(t *testing.T) { require.NoError(t, err) lines := 0 - sc := bufio.NewScanner(strings.NewReader(string(data))) + sc := bufio.NewScanner(core.NewReader(string(data))) for sc.Scan() { lines++ } diff --git a/manifest/compile.go b/manifest/compile.go index d9f00ea..b168285 100644 --- a/manifest/compile.go +++ b/manifest/compile.go @@ -3,9 +3,9 @@ package manifest import ( "crypto/ed25519" "encoding/json" - "path/filepath" "time" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "dappco.re/go/core/io" ) @@ -83,13 +83,13 @@ func WriteCompiled(medium io.Medium, root string, cm *CompiledManifest) error { if err != nil { return coreerr.E("manifest.WriteCompiled", "marshal failed", err) } - path := filepath.Join(root, compiledPath) + path := core.JoinPath(root, compiledPath) return medium.Write(path, string(data)) } // LoadCompiled reads and parses a core.json from the given root directory. func LoadCompiled(medium io.Medium, root string) (*CompiledManifest, error) { - path := filepath.Join(root, compiledPath) + path := core.JoinPath(root, compiledPath) data, err := medium.Read(path) if err != nil { return nil, coreerr.E("manifest.LoadCompiled", "read failed", err) diff --git a/marketplace/builder.go b/marketplace/builder.go index c85f182..d55b3b4 100644 --- a/marketplace/builder.go +++ b/marketplace/builder.go @@ -4,9 +4,9 @@ import ( "encoding/json" "log" "os" - "path/filepath" "sort" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" coreio "dappco.re/go/core/io" "dappco.re/go/core/scm/manifest" @@ -48,7 +48,7 @@ func (b *Builder) BuildFromDirs(dirs ...string) (*Index, error) { continue } - m, err := b.loadFromDir(filepath.Join(dir, e.Name())) + m, err := b.loadFromDir(core.JoinPath(dir, e.Name())) if err != nil { log.Printf("marketplace: skipping %s: %v", e.Name(), err) continue @@ -114,7 +114,7 @@ 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 { + if err := coreio.Local.EnsureDir(core.PathDir(path)); err != nil { return coreerr.E("marketplace.WriteIndex", "mkdir failed", err) } data, err := json.MarshalIndent(idx, "", " ") @@ -127,7 +127,7 @@ func WriteIndex(path string, idx *Index) error { // loadFromDir tries core.json first, then falls back to .core/manifest.yaml. func (b *Builder) loadFromDir(dir string) (*manifest.Manifest, error) { // Prefer compiled manifest (core.json). - coreJSON := filepath.Join(dir, "core.json") + coreJSON := core.JoinPath(dir, "core.json") if raw, err := coreio.Local.Read(coreJSON); err == nil { cm, err := manifest.ParseCompiled([]byte(raw)) if err != nil { @@ -137,7 +137,7 @@ func (b *Builder) loadFromDir(dir string) (*manifest.Manifest, error) { } // Fall back to source manifest. - manifestYAML := filepath.Join(dir, ".core", "manifest.yaml") + manifestYAML := core.JoinPath(dir, ".core", "manifest.yaml") raw, err := coreio.Local.Read(manifestYAML) if err != nil { return nil, nil // No manifest — skip silently. diff --git a/marketplace/builder_test.go b/marketplace/builder_test.go index baf3121..0489b96 100644 --- a/marketplace/builder_test.go +++ b/marketplace/builder_test.go @@ -3,9 +3,9 @@ package marketplace import ( "encoding/json" "os" - "path/filepath" "testing" + core "dappco.re/go/core" "dappco.re/go/core/scm/manifest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -14,10 +14,10 @@ import ( // writeManifestYAML writes a .core/manifest.yaml for a module directory. func writeManifestYAML(t *testing.T, dir, code, name, version string) { t.Helper() - coreDir := filepath.Join(dir, ".core") + coreDir := core.JoinPath(dir, ".core") require.NoError(t, os.MkdirAll(coreDir, 0755)) yaml := "code: " + code + "\nname: " + name + "\nversion: " + version + "\n" - require.NoError(t, os.WriteFile(filepath.Join(coreDir, "manifest.yaml"), []byte(yaml), 0644)) + require.NoError(t, os.WriteFile(core.JoinPath(coreDir, "manifest.yaml"), []byte(yaml), 0644)) } // writeCoreJSON writes a core.json for a module directory. @@ -33,12 +33,12 @@ func writeCoreJSON(t *testing.T, dir, code, name, version string) { } data, err := json.Marshal(cm) require.NoError(t, err) - require.NoError(t, os.WriteFile(filepath.Join(dir, "core.json"), data, 0644)) + require.NoError(t, os.WriteFile(core.JoinPath(dir, "core.json"), data, 0644)) } func TestBuildFromDirs_Good_ManifestYAML(t *testing.T) { root := t.TempDir() - modDir := filepath.Join(root, "my-widget") + modDir := core.JoinPath(root, "my-widget") require.NoError(t, os.MkdirAll(modDir, 0755)) writeManifestYAML(t, modDir, "my-widget", "My Widget", "1.0.0") @@ -55,7 +55,7 @@ func TestBuildFromDirs_Good_ManifestYAML(t *testing.T) { func TestBuildFromDirs_Good_CoreJSON(t *testing.T) { root := t.TempDir() - modDir := filepath.Join(root, "compiled-mod") + modDir := core.JoinPath(root, "compiled-mod") require.NoError(t, os.MkdirAll(modDir, 0755)) writeCoreJSON(t, modDir, "compiled-mod", "Compiled Module", "2.0.0") @@ -70,7 +70,7 @@ func TestBuildFromDirs_Good_CoreJSON(t *testing.T) { func TestBuildFromDirs_Good_PrefersCompiledOverSource(t *testing.T) { root := t.TempDir() - modDir := filepath.Join(root, "dual-mod") + modDir := core.JoinPath(root, "dual-mod") require.NoError(t, os.MkdirAll(modDir, 0755)) writeManifestYAML(t, modDir, "source-code", "Source Name", "1.0.0") writeCoreJSON(t, modDir, "compiled-code", "Compiled Name", "2.0.0") @@ -87,9 +87,9 @@ func TestBuildFromDirs_Good_PrefersCompiledOverSource(t *testing.T) { func TestBuildFromDirs_Good_SkipsNoManifest(t *testing.T) { root := t.TempDir() // Directory with no manifest. - require.NoError(t, os.MkdirAll(filepath.Join(root, "no-manifest"), 0755)) + require.NoError(t, os.MkdirAll(core.JoinPath(root, "no-manifest"), 0755)) // Directory with a manifest. - modDir := filepath.Join(root, "has-manifest") + modDir := core.JoinPath(root, "has-manifest") require.NoError(t, os.MkdirAll(modDir, 0755)) writeManifestYAML(t, modDir, "has-manifest", "Has Manifest", "0.1.0") @@ -103,8 +103,8 @@ func TestBuildFromDirs_Good_Deduplicates(t *testing.T) { dir1 := t.TempDir() dir2 := t.TempDir() - mod1 := filepath.Join(dir1, "shared") - mod2 := filepath.Join(dir2, "shared") + mod1 := core.JoinPath(dir1, "shared") + mod2 := core.JoinPath(dir2, "shared") require.NoError(t, os.MkdirAll(mod1, 0755)) require.NoError(t, os.MkdirAll(mod2, 0755)) writeManifestYAML(t, mod1, "shared", "Shared V1", "1.0.0") @@ -121,7 +121,7 @@ func TestBuildFromDirs_Good_Deduplicates(t *testing.T) { func TestBuildFromDirs_Good_SortsByCode(t *testing.T) { root := t.TempDir() for _, name := range []string{"charlie", "alpha", "bravo"} { - d := filepath.Join(root, name) + d := core.JoinPath(root, name) require.NoError(t, os.MkdirAll(d, 0755)) writeManifestYAML(t, d, name, name, "1.0.0") } @@ -153,7 +153,7 @@ func TestBuildFromDirs_Good_NonexistentDir(t *testing.T) { func TestBuildFromDirs_Good_NoRepoURLWithoutConfig(t *testing.T) { root := t.TempDir() - modDir := filepath.Join(root, "mod") + modDir := core.JoinPath(root, "mod") require.NoError(t, os.MkdirAll(modDir, 0755)) writeManifestYAML(t, modDir, "mod", "Module", "1.0.0") @@ -197,7 +197,7 @@ func TestBuildFromManifests_Good_Deduplicates(t *testing.T) { func TestWriteIndex_Good(t *testing.T) { dir := t.TempDir() - path := filepath.Join(dir, "marketplace", "index.json") + path := core.JoinPath(dir, "marketplace", "index.json") idx := &Index{ Version: 1, @@ -220,10 +220,10 @@ func TestWriteIndex_Good(t *testing.T) { func TestWriteIndex_Good_RoundTrip(t *testing.T) { dir := t.TempDir() - path := filepath.Join(dir, "index.json") + path := core.JoinPath(dir, "index.json") root := t.TempDir() - modDir := filepath.Join(root, "roundtrip") + modDir := core.JoinPath(root, "roundtrip") require.NoError(t, os.MkdirAll(modDir, 0755)) writeManifestYAML(t, modDir, "roundtrip", "Roundtrip Module", "3.0.0") diff --git a/marketplace/discovery.go b/marketplace/discovery.go index e9e8d89..03716e5 100644 --- a/marketplace/discovery.go +++ b/marketplace/discovery.go @@ -3,8 +3,8 @@ package marketplace import ( "log" "os" - "path/filepath" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" coreio "dappco.re/go/core/io" "dappco.re/go/core/scm/manifest" @@ -39,8 +39,8 @@ func DiscoverProviders(dir string) ([]DiscoveredProvider, error) { continue } - providerDir := filepath.Join(dir, e.Name()) - manifestPath := filepath.Join(providerDir, ".core", "manifest.yaml") + providerDir := core.JoinPath(dir, e.Name()) + manifestPath := core.JoinPath(providerDir, ".core", "manifest.yaml") raw, err := coreio.Local.Read(manifestPath) if err != nil { @@ -110,7 +110,7 @@ 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 { + if err := coreio.Local.EnsureDir(core.PathDir(path)); err != nil { return coreerr.E("marketplace.SaveProviderRegistry", "ensure directory", err) } diff --git a/marketplace/discovery_test.go b/marketplace/discovery_test.go index c96c9c8..1bda16a 100644 --- a/marketplace/discovery_test.go +++ b/marketplace/discovery_test.go @@ -1,8 +1,8 @@ package marketplace import ( + core "dappco.re/go/core" "os" - "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -12,11 +12,11 @@ import ( // createProviderDir creates a provider directory with a .core/manifest.yaml. func createProviderDir(t *testing.T, baseDir, code string, manifestYAML string) string { t.Helper() - provDir := filepath.Join(baseDir, code) - coreDir := filepath.Join(provDir, ".core") + provDir := core.JoinPath(baseDir, code) + coreDir := core.JoinPath(provDir, ".core") require.NoError(t, os.MkdirAll(coreDir, 0755)) require.NoError(t, os.WriteFile( - filepath.Join(coreDir, "manifest.yaml"), + core.JoinPath(coreDir, "manifest.yaml"), []byte(manifestYAML), 0644, )) return provDir @@ -85,7 +85,7 @@ func TestDiscoverProviders_Good_SkipNoManifest(t *testing.T) { dir := t.TempDir() // Directory with no manifest. - require.NoError(t, os.MkdirAll(filepath.Join(dir, "no-manifest"), 0755)) + require.NoError(t, os.MkdirAll(core.JoinPath(dir, "no-manifest"), 0755)) // Directory with a valid provider manifest. createProviderDir(t, dir, "good-provider", ` @@ -106,11 +106,11 @@ func TestDiscoverProviders_Good_SkipInvalidManifest(t *testing.T) { dir := t.TempDir() // Directory with invalid YAML. - provDir := filepath.Join(dir, "bad-yaml") - coreDir := filepath.Join(provDir, ".core") + provDir := core.JoinPath(dir, "bad-yaml") + coreDir := core.JoinPath(provDir, ".core") require.NoError(t, os.MkdirAll(coreDir, 0755)) require.NoError(t, os.WriteFile( - filepath.Join(coreDir, "manifest.yaml"), + core.JoinPath(coreDir, "manifest.yaml"), []byte("not: valid: yaml: ["), 0644, )) @@ -137,7 +137,7 @@ func TestDiscoverProviders_Good_SkipFiles(t *testing.T) { dir := t.TempDir() // Create a regular file (not a directory). - require.NoError(t, os.WriteFile(filepath.Join(dir, "readme.md"), []byte("# readme"), 0644)) + require.NoError(t, os.WriteFile(core.JoinPath(dir, "readme.md"), []byte("# readme"), 0644)) providers, err := DiscoverProviders(dir) require.NoError(t, err) @@ -158,13 +158,13 @@ binary: ./test-prov providers, err := DiscoverProviders(dir) require.NoError(t, err) require.Len(t, providers, 1) - assert.Equal(t, filepath.Join(dir, "test-prov"), providers[0].Dir) + assert.Equal(t, core.JoinPath(dir, "test-prov"), providers[0].Dir) } // -- ProviderRegistryFile tests ----------------------------------------------- func TestProviderRegistry_LoadSave_Good(t *testing.T) { - path := filepath.Join(t.TempDir(), "registry.yaml") + path := core.JoinPath(t.TempDir(), "registry.yaml") reg := &ProviderRegistryFile{ Version: 1, diff --git a/marketplace/installer.go b/marketplace/installer.go index e338ce4..02bfcfd 100644 --- a/marketplace/installer.go +++ b/marketplace/installer.go @@ -3,14 +3,14 @@ package marketplace import ( "context" "encoding/hex" - "encoding/json" "os/exec" - "path/filepath" - "strings" "time" "dappco.re/go/core/io" "dappco.re/go/core/io/store" + "encoding/json" + + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "dappco.re/go/core/scm/agentci" "dappco.re/go/core/scm/manifest" @@ -83,7 +83,7 @@ func (i *Installer) Install(ctx context.Context, mod Module) error { return err } - entryPoint := filepath.Join(dest, "main.ts") + entryPoint := core.JoinPath(dest, "main.ts") installed := InstalledModule{ Code: safeCode, Name: m.Name, @@ -145,7 +145,7 @@ func (i *Installer) Update(ctx context.Context, code string) error { cmd := exec.CommandContext(ctx, "git", "-C", dest, "pull", "--ff-only") if output, err := cmd.CombinedOutput(); err != nil { - return coreerr.E("marketplace.Installer.Update", "pull: "+strings.TrimSpace(string(output)), err) + return coreerr.E("marketplace.Installer.Update", "pull: "+core.Trim(string(output)), err) } // Reload and re-verify manifest with the same key used at install time @@ -206,7 +206,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 coreerr.E("marketplace.gitClone", strings.TrimSpace(string(output)), err) + return coreerr.E("marketplace.gitClone", core.Trim(string(output)), err) } return nil } diff --git a/marketplace/installer_test.go b/marketplace/installer_test.go index ee992fa..023267a 100644 --- a/marketplace/installer_test.go +++ b/marketplace/installer_test.go @@ -6,9 +6,9 @@ import ( "encoding/hex" "os" "os/exec" - "path/filepath" "testing" + core "dappco.re/go/core" "dappco.re/go/core/io" "dappco.re/go/core/io/store" "dappco.re/go/core/scm/manifest" @@ -20,16 +20,16 @@ import ( // Returns the repo path (usable as Module.Repo for local clone). func createTestRepo(t *testing.T, code, version string) string { t.Helper() - dir := filepath.Join(t.TempDir(), code) - require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0755)) + dir := core.JoinPath(t.TempDir(), code) + require.NoError(t, os.MkdirAll(core.JoinPath(dir, ".core"), 0755)) manifestYAML := "code: " + code + "\nname: Test " + code + "\nversion: \"" + version + "\"\n" require.NoError(t, os.WriteFile( - filepath.Join(dir, ".core", "manifest.yaml"), + core.JoinPath(dir, ".core", "manifest.yaml"), []byte(manifestYAML), 0644, )) require.NoError(t, os.WriteFile( - filepath.Join(dir, "main.ts"), + core.JoinPath(dir, "main.ts"), []byte("export async function init(core: any) {}\n"), 0644, )) @@ -46,8 +46,8 @@ func createSignedTestRepo(t *testing.T, code, version string) (string, string) { pub, priv, err := ed25519.GenerateKey(nil) require.NoError(t, err) - dir := filepath.Join(t.TempDir(), code) - require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0755)) + dir := core.JoinPath(t.TempDir(), code) + require.NoError(t, os.MkdirAll(core.JoinPath(dir, ".core"), 0755)) m := &manifest.Manifest{ Code: code, @@ -58,8 +58,8 @@ func createSignedTestRepo(t *testing.T, code, version string) (string, string) { data, err := manifest.MarshalYAML(m) require.NoError(t, err) - require.NoError(t, os.WriteFile(filepath.Join(dir, ".core", "manifest.yaml"), data, 0644)) - require.NoError(t, os.WriteFile(filepath.Join(dir, "main.ts"), []byte("export async function init(core: any) {}\n"), 0644)) + require.NoError(t, os.WriteFile(core.JoinPath(dir, ".core", "manifest.yaml"), data, 0644)) + require.NoError(t, os.WriteFile(core.JoinPath(dir, "main.ts"), []byte("export async function init(core: any) {}\n"), 0644)) runGit(t, dir, "init") runGit(t, dir, "add", "--force", ".") @@ -77,7 +77,7 @@ func runGit(t *testing.T, dir string, args ...string) { func TestInstall_Good(t *testing.T) { repo := createTestRepo(t, "hello-mod", "1.0") - modulesDir := filepath.Join(t.TempDir(), "modules") + modulesDir := core.JoinPath(t.TempDir(), "modules") st, err := store.New(":memory:") require.NoError(t, err) @@ -91,7 +91,7 @@ func TestInstall_Good(t *testing.T) { require.NoError(t, err) // Verify directory exists - _, err = os.Stat(filepath.Join(modulesDir, "hello-mod", "main.ts")) + _, err = os.Stat(core.JoinPath(modulesDir, "hello-mod", "main.ts")) assert.NoError(t, err, "main.ts should exist in installed module") // Verify store entry @@ -103,7 +103,7 @@ func TestInstall_Good(t *testing.T) { func TestInstall_Good_Signed(t *testing.T) { repo, signKey := createSignedTestRepo(t, "signed-mod", "2.0") - modulesDir := filepath.Join(t.TempDir(), "modules") + modulesDir := core.JoinPath(t.TempDir(), "modules") st, err := store.New(":memory:") require.NoError(t, err) @@ -124,7 +124,7 @@ func TestInstall_Good_Signed(t *testing.T) { func TestInstall_Bad_AlreadyInstalled(t *testing.T) { repo := createTestRepo(t, "dup-mod", "1.0") - modulesDir := filepath.Join(t.TempDir(), "modules") + modulesDir := core.JoinPath(t.TempDir(), "modules") st, err := store.New(":memory:") require.NoError(t, err) @@ -144,7 +144,7 @@ func TestInstall_Bad_InvalidSignature(t *testing.T) { repo, _ := createSignedTestRepo(t, "bad-sig", "1.0") _, wrongKey := createSignedTestRepo(t, "dummy", "1.0") // different key - modulesDir := filepath.Join(t.TempDir(), "modules") + modulesDir := core.JoinPath(t.TempDir(), "modules") st, err := store.New(":memory:") require.NoError(t, err) @@ -159,13 +159,13 @@ func TestInstall_Bad_InvalidSignature(t *testing.T) { assert.Error(t, err) // Verify directory was cleaned up - _, statErr := os.Stat(filepath.Join(modulesDir, "bad-sig")) + _, statErr := os.Stat(core.JoinPath(modulesDir, "bad-sig")) assert.True(t, os.IsNotExist(statErr), "directory should be cleaned up on failure") } func TestInstall_Bad_PathTraversalCode(t *testing.T) { repo := createTestRepo(t, "safe-mod", "1.0") - modulesDir := filepath.Join(t.TempDir(), "modules") + modulesDir := core.JoinPath(t.TempDir(), "modules") st, err := store.New(":memory:") require.NoError(t, err) @@ -182,13 +182,13 @@ func TestInstall_Bad_PathTraversalCode(t *testing.T) { _, err = st.Get("_modules", "escape") assert.Error(t, err) - _, err = os.Stat(filepath.Join(filepath.Dir(modulesDir), "escape")) + _, err = os.Stat(core.JoinPath(core.PathDir(modulesDir), "escape")) assert.True(t, os.IsNotExist(err)) } func TestRemove_Good(t *testing.T) { repo := createTestRepo(t, "rm-mod", "1.0") - modulesDir := filepath.Join(t.TempDir(), "modules") + modulesDir := core.JoinPath(t.TempDir(), "modules") st, err := store.New(":memory:") require.NoError(t, err) @@ -201,7 +201,7 @@ func TestRemove_Good(t *testing.T) { require.NoError(t, err) // Directory gone - _, statErr := os.Stat(filepath.Join(modulesDir, "rm-mod")) + _, statErr := os.Stat(core.JoinPath(modulesDir, "rm-mod")) assert.True(t, os.IsNotExist(statErr)) // Store entry gone @@ -222,8 +222,8 @@ func TestRemove_Bad_NotInstalled(t *testing.T) { func TestRemove_Bad_PathTraversalCode(t *testing.T) { baseDir := t.TempDir() - modulesDir := filepath.Join(baseDir, "modules") - escapeDir := filepath.Join(baseDir, "escape") + modulesDir := core.JoinPath(baseDir, "modules") + escapeDir := core.JoinPath(baseDir, "escape") require.NoError(t, os.MkdirAll(escapeDir, 0755)) st, err := store.New(":memory:") @@ -241,7 +241,7 @@ func TestRemove_Bad_PathTraversalCode(t *testing.T) { } func TestInstalled_Good(t *testing.T) { - modulesDir := filepath.Join(t.TempDir(), "modules") + modulesDir := core.JoinPath(t.TempDir(), "modules") st, err := store.New(":memory:") require.NoError(t, err) @@ -280,7 +280,7 @@ func TestInstalled_Good_Empty(t *testing.T) { func TestUpdate_Good(t *testing.T) { repo := createTestRepo(t, "upd-mod", "1.0") - modulesDir := filepath.Join(t.TempDir(), "modules") + modulesDir := core.JoinPath(t.TempDir(), "modules") st, err := store.New(":memory:") require.NoError(t, err) @@ -291,7 +291,7 @@ func TestUpdate_Good(t *testing.T) { // Update the origin repo newManifest := "code: upd-mod\nname: Updated Module\nversion: \"2.0\"\n" - require.NoError(t, os.WriteFile(filepath.Join(repo, ".core", "manifest.yaml"), []byte(newManifest), 0644)) + require.NoError(t, os.WriteFile(core.JoinPath(repo, ".core", "manifest.yaml"), []byte(newManifest), 0644)) runGit(t, repo, "add", ".") runGit(t, repo, "commit", "-m", "bump version") @@ -307,7 +307,7 @@ func TestUpdate_Good(t *testing.T) { } func TestUpdate_Bad_PathTraversalCode(t *testing.T) { - modulesDir := filepath.Join(t.TempDir(), "modules") + modulesDir := core.JoinPath(t.TempDir(), "modules") st, err := store.New(":memory:") require.NoError(t, err) diff --git a/marketplace/marketplace.go b/marketplace/marketplace.go index c97fe39..add1d02 100644 --- a/marketplace/marketplace.go +++ b/marketplace/marketplace.go @@ -2,8 +2,8 @@ package marketplace import ( "encoding/json" - "strings" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -34,12 +34,12 @@ func ParseIndex(data []byte) (*Index, error) { // Search returns modules matching the query in code, name, or category. func (idx *Index) Search(query string) []Module { - q := strings.ToLower(query) + q := core.Lower(query) var results []Module for _, m := range idx.Modules { - if strings.Contains(strings.ToLower(m.Code), q) || - strings.Contains(strings.ToLower(m.Name), q) || - strings.Contains(strings.ToLower(m.Category), q) { + if core.Contains(core.Lower(m.Code), q) || + core.Contains(core.Lower(m.Name), q) || + core.Contains(core.Lower(m.Category), q) { results = append(results, m) } } diff --git a/plugin/installer.go b/plugin/installer.go index 0be3233..7af1da0 100644 --- a/plugin/installer.go +++ b/plugin/installer.go @@ -2,13 +2,12 @@ package plugin import ( "context" - "fmt" "net/url" "os/exec" - "path/filepath" "strings" "time" + core "dappco.re/go/core" "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" "dappco.re/go/core/scm/agentci" @@ -55,7 +54,7 @@ func (i *Installer) Install(ctx context.Context, source string) error { } // Load and validate manifest - manifestPath := filepath.Join(pluginDir, "plugin.json") + manifestPath := core.JoinPath(pluginDir, "plugin.json") manifest, err := LoadManifest(i.medium, manifestPath) if err != nil { // Clean up on failure @@ -77,7 +76,7 @@ func (i *Installer) Install(ctx context.Context, source string) error { cfg := &PluginConfig{ Name: manifest.Name, Version: version, - Source: fmt.Sprintf("github:%s/%s", org, repo), + Source: core.Sprintf("github:%s/%s", org, repo), Enabled: true, InstalledAt: time.Now().UTC().Format(time.RFC3339), } @@ -108,11 +107,11 @@ func (i *Installer) Update(ctx context.Context, name string) error { // Pull latest changes cmd := exec.CommandContext(ctx, "git", "-C", pluginDir, "pull", "--ff-only") if output, err := cmd.CombinedOutput(); err != nil { - return coreerr.E("plugin.Installer.Update", "failed to pull updates: "+strings.TrimSpace(string(output)), err) + return coreerr.E("plugin.Installer.Update", "failed to pull updates: "+core.Trim(string(output)), err) } // Reload manifest to get updated version - manifestPath := filepath.Join(pluginDir, "plugin.json") + manifestPath := core.JoinPath(pluginDir, "plugin.json") manifest, err := LoadManifest(i.medium, manifestPath) if err != nil { return coreerr.E("plugin.Installer.Update", "failed to read updated manifest", err) @@ -159,7 +158,7 @@ func (i *Installer) Remove(name string) error { // cloneRepo clones a GitHub repository using the gh CLI. func (i *Installer) cloneRepo(ctx context.Context, org, repo, version, dest string) error { - repoURL := fmt.Sprintf("%s/%s", org, repo) + repoURL := core.Sprintf("%s/%s", org, repo) args := []string{"repo", "clone", repoURL, dest} if version != "" { @@ -168,7 +167,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 coreerr.E("plugin.Installer.cloneRepo", strings.TrimSpace(string(output)), err) + return coreerr.E("plugin.Installer.cloneRepo", core.Trim(string(output)), err) } return nil @@ -199,7 +198,7 @@ func ParseSource(source string) (org, repo, version string, err error) { } // Split org/repo - parts := strings.Split(path, "/") + parts := core.Split(path, "/") if len(parts) != 2 || parts[0] == "" || parts[1] == "" { return "", "", "", coreerr.E("plugin.ParseSource", "source must be in format org/repo[@version]", nil) } diff --git a/plugin/loader.go b/plugin/loader.go index 3362886..a548a99 100644 --- a/plugin/loader.go +++ b/plugin/loader.go @@ -1,8 +1,8 @@ package plugin import ( - "path/filepath" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "dappco.re/go/core/io" ) @@ -49,7 +49,7 @@ func (l *Loader) Discover() ([]*Manifest, error) { // LoadPlugin loads a single plugin's manifest by name. func (l *Loader) LoadPlugin(name string) (*Manifest, error) { - manifestPath := filepath.Join(l.baseDir, name, "plugin.json") + manifestPath := core.JoinPath(l.baseDir, name, "plugin.json") manifest, err := LoadManifest(l.medium, manifestPath) if err != nil { return nil, coreerr.E("plugin.Loader.LoadPlugin", "failed to load plugin: "+name, err) diff --git a/plugin/registry.go b/plugin/registry.go index f81a025..c9e54e8 100644 --- a/plugin/registry.go +++ b/plugin/registry.go @@ -3,9 +3,9 @@ package plugin import ( "cmp" "encoding/json" - "path/filepath" "slices" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "dappco.re/go/core/io" ) @@ -68,7 +68,7 @@ func (r *Registry) Remove(name string) error { // registryPath returns the full path to the registry file. func (r *Registry) registryPath() string { - return filepath.Join(r.basePath, registryFilename) + return core.JoinPath(r.basePath, registryFilename) } // Load reads the plugin registry from disk. diff --git a/repos/gitstate.go b/repos/gitstate.go index 15d8436..5743c9a 100644 --- a/repos/gitstate.go +++ b/repos/gitstate.go @@ -1,9 +1,9 @@ package repos import ( - "path/filepath" "time" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "dappco.re/go/core/io" "gopkg.in/yaml.v3" @@ -36,7 +36,7 @@ type AgentState struct { // LoadGitState reads .core/git.yaml from the given workspace root directory. // Returns a new empty GitState if the file does not exist. func LoadGitState(m io.Medium, root string) (*GitState, error) { - path := filepath.Join(root, ".core", "git.yaml") + path := core.JoinPath(root, ".core", "git.yaml") if !m.Exists(path) { return NewGitState(), nil @@ -64,7 +64,7 @@ func LoadGitState(m io.Medium, root string) (*GitState, error) { // SaveGitState writes .core/git.yaml to the given workspace root directory. func SaveGitState(m io.Medium, root string, gs *GitState) error { - coreDir := filepath.Join(root, ".core") + coreDir := core.JoinPath(root, ".core") if err := m.EnsureDir(coreDir); err != nil { return coreerr.E("repos.SaveGitState", "failed to create .core directory", err) } @@ -74,7 +74,7 @@ func SaveGitState(m io.Medium, root string, gs *GitState) error { return coreerr.E("repos.SaveGitState", "failed to marshal git state", err) } - path := filepath.Join(coreDir, "git.yaml") + path := core.JoinPath(coreDir, "git.yaml") if err := m.Write(path, string(data)); err != nil { return coreerr.E("repos.SaveGitState", "failed to write git state", err) } diff --git a/repos/kbconfig.go b/repos/kbconfig.go index fd8ed3a..e55849f 100644 --- a/repos/kbconfig.go +++ b/repos/kbconfig.go @@ -1,9 +1,7 @@ package repos import ( - "fmt" - "path/filepath" - + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "dappco.re/go/core/io" "gopkg.in/yaml.v3" @@ -67,7 +65,7 @@ func DefaultKBConfig() *KBConfig { // LoadKBConfig reads .core/kb.yaml from the given workspace root directory. // Returns defaults if the file does not exist. func LoadKBConfig(m io.Medium, root string) (*KBConfig, error) { - path := filepath.Join(root, ".core", "kb.yaml") + path := core.JoinPath(root, ".core", "kb.yaml") if !m.Exists(path) { return DefaultKBConfig(), nil @@ -88,7 +86,7 @@ func LoadKBConfig(m io.Medium, root string) (*KBConfig, error) { // SaveKBConfig writes .core/kb.yaml to the given workspace root directory. func SaveKBConfig(m io.Medium, root string, kb *KBConfig) error { - coreDir := filepath.Join(root, ".core") + coreDir := core.JoinPath(root, ".core") if err := m.EnsureDir(coreDir); err != nil { return coreerr.E("repos.SaveKBConfig", "failed to create .core directory", err) } @@ -98,7 +96,7 @@ func SaveKBConfig(m io.Medium, root string, kb *KBConfig) error { return coreerr.E("repos.SaveKBConfig", "failed to marshal kb config", err) } - path := filepath.Join(coreDir, "kb.yaml") + path := core.JoinPath(coreDir, "kb.yaml") if err := m.Write(path, string(data)); err != nil { return coreerr.E("repos.SaveKBConfig", "failed to write kb config", err) } @@ -108,10 +106,10 @@ func SaveKBConfig(m io.Medium, root string, kb *KBConfig) error { // WikiRepoURL returns the full clone URL for a repo's wiki. func (kb *KBConfig) WikiRepoURL(repoName string) string { - return fmt.Sprintf("%s/%s.wiki.git", kb.Wiki.Remote, repoName) + return core.Sprintf("%s/%s.wiki.git", kb.Wiki.Remote, repoName) } // WikiLocalPath returns the local path for a repo's wiki clone. func (kb *KBConfig) WikiLocalPath(root, repoName string) string { - return filepath.Join(root, ".core", kb.Wiki.Dir, repoName) + return core.JoinPath(root, ".core", kb.Wiki.Dir, repoName) } diff --git a/repos/registry.go b/repos/registry.go index f6af2f7..acbde14 100644 --- a/repos/registry.go +++ b/repos/registry.go @@ -5,9 +5,8 @@ package repos import ( "os" - "path/filepath" - "strings" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "dappco.re/go/core/io" "gopkg.in/yaml.v3" @@ -84,7 +83,7 @@ func LoadRegistry(m io.Medium, path string) (*Registry, error) { for name, repo := range reg.Repos { repo.Name = name if repo.Path == "" { - repo.Path = filepath.Join(reg.BasePath, name) + repo.Path = core.JoinPath(reg.BasePath, name) } else { repo.Path = expandPath(repo.Path) } @@ -111,17 +110,17 @@ func FindRegistry(m io.Medium) (string, error) { for { // Check repos.yaml (existing) - candidate := filepath.Join(dir, "repos.yaml") + candidate := core.JoinPath(dir, "repos.yaml") if m.Exists(candidate) { return candidate, nil } // Check .core/repos.yaml (new) - candidate = filepath.Join(dir, ".core", "repos.yaml") + candidate = core.JoinPath(dir, ".core", "repos.yaml") if m.Exists(candidate) { return candidate, nil } - parent := filepath.Dir(dir) + parent := core.PathDir(dir) if parent == dir { break } @@ -135,9 +134,9 @@ func FindRegistry(m io.Medium) (string, error) { } commonPaths := []string{ - filepath.Join(home, "Code", "host-uk", ".core", "repos.yaml"), - filepath.Join(home, "Code", "host-uk", "repos.yaml"), - filepath.Join(home, ".config", "core", "repos.yaml"), + core.JoinPath(home, "Code", "host-uk", ".core", "repos.yaml"), + core.JoinPath(home, "Code", "host-uk", "repos.yaml"), + core.JoinPath(home, ".config", "core", "repos.yaml"), } for _, p := range commonPaths { @@ -171,8 +170,8 @@ func ScanDirectory(m io.Medium, dir string) (*Registry, error) { continue } - repoPath := filepath.Join(dir, entry.Name()) - gitPath := filepath.Join(repoPath, ".git") + repoPath := core.JoinPath(dir, entry.Name()) + gitPath := core.JoinPath(repoPath, ".git") if !m.IsDir(gitPath) { continue // Not a git repo @@ -199,25 +198,25 @@ func ScanDirectory(m io.Medium, dir string) (*Registry, error) { // detectOrg tries to extract the GitHub org from a repo's origin remote. func detectOrg(m io.Medium, repoPath string) string { // Try to read git remote - configPath := filepath.Join(repoPath, ".git", "config") + configPath := core.JoinPath(repoPath, ".git", "config") content, err := m.Read(configPath) if err != nil { return "" } // Look for patterns like github.com:org/repo or github.com/org/repo - for _, line := range strings.Split(content, "\n") { - line = strings.TrimSpace(line) - if !strings.HasPrefix(line, "url = ") { + for _, line := range core.Split(content, "\n") { + line = core.Trim(line) + if !core.HasPrefix(line, "url = ") { continue } - url := strings.TrimPrefix(line, "url = ") + url := core.TrimPrefix(line, "url = ") // git@github.com:org/repo.git - if strings.Contains(url, "github.com:") { - parts := strings.Split(url, ":") + if core.Contains(url, "github.com:") { + parts := core.Split(url, ":") if len(parts) >= 2 { - orgRepo := strings.TrimSuffix(parts[1], ".git") - orgParts := strings.Split(orgRepo, "/") + orgRepo := core.TrimSuffix(parts[1], ".git") + orgParts := core.Split(orgRepo, "/") if len(orgParts) >= 1 { return orgParts[0] } @@ -225,11 +224,11 @@ func detectOrg(m io.Medium, repoPath string) string { } // https://github.com/org/repo.git - if strings.Contains(url, "github.com/") { - parts := strings.Split(url, "github.com/") + if core.Contains(url, "github.com/") { + parts := core.Split(url, "github.com/") if len(parts) >= 2 { - orgRepo := strings.TrimSuffix(parts[1], ".git") - orgParts := strings.Split(orgRepo, "/") + orgRepo := core.TrimSuffix(parts[1], ".git") + orgParts := core.Split(orgRepo, "/") if len(orgParts) >= 1 { return orgParts[0] } @@ -317,7 +316,7 @@ func (repo *Repo) Exists() bool { // IsGitRepo checks if the repo directory contains a .git folder. func (repo *Repo) IsGitRepo() bool { - gitPath := filepath.Join(repo.Path, ".git") + gitPath := core.JoinPath(repo.Path, ".git") return repo.getMedium().IsDir(gitPath) } @@ -330,12 +329,12 @@ func (repo *Repo) getMedium() io.Medium { // expandPath expands ~ to home directory. func expandPath(path string) string { - if strings.HasPrefix(path, "~/") { + if core.HasPrefix(path, "~/") { home, err := os.UserHomeDir() if err != nil { return path } - return filepath.Join(home, path[2:]) + return core.JoinPath(home, path[2:]) } return path } diff --git a/repos/workconfig.go b/repos/workconfig.go index 7452245..928ea03 100644 --- a/repos/workconfig.go +++ b/repos/workconfig.go @@ -1,9 +1,9 @@ package repos import ( - "path/filepath" "time" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "dappco.re/go/core/io" "gopkg.in/yaml.v3" @@ -55,7 +55,7 @@ func DefaultWorkConfig() *WorkConfig { // LoadWorkConfig reads .core/work.yaml from the given workspace root directory. // Returns defaults if the file does not exist. func LoadWorkConfig(m io.Medium, root string) (*WorkConfig, error) { - path := filepath.Join(root, ".core", "work.yaml") + path := core.JoinPath(root, ".core", "work.yaml") if !m.Exists(path) { return DefaultWorkConfig(), nil @@ -76,7 +76,7 @@ func LoadWorkConfig(m io.Medium, root string) (*WorkConfig, error) { // SaveWorkConfig writes .core/work.yaml to the given workspace root directory. func SaveWorkConfig(m io.Medium, root string, wc *WorkConfig) error { - coreDir := filepath.Join(root, ".core") + coreDir := core.JoinPath(root, ".core") if err := m.EnsureDir(coreDir); err != nil { return coreerr.E("repos.SaveWorkConfig", "failed to create .core directory", err) } @@ -86,7 +86,7 @@ func SaveWorkConfig(m io.Medium, root string, wc *WorkConfig) error { return coreerr.E("repos.SaveWorkConfig", "failed to marshal work config", err) } - path := filepath.Join(coreDir, "work.yaml") + path := core.JoinPath(coreDir, "work.yaml") if err := m.Write(path, string(data)); err != nil { return coreerr.E("repos.SaveWorkConfig", "failed to write work config", err) }