feat: upgrade to core v0.8.0-alpha.1, replace banned stdlib imports
Migrate git/service.go to Action-based API. Replace fmt, strings, path/filepath with Core primitives across 77 files (~400 call sites). Keep encoding/json, strings.EqualFold/SplitSeq/Fields, filepath.Abs/Rel. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4733374cc0
commit
d67ac486ff
81 changed files with 645 additions and 642 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(`<html><body>`)
|
||||
for i := range count {
|
||||
page.WriteString(fmt.Sprintf(`
|
||||
page.WriteString(core.Sprintf(`
|
||||
<div class="post">
|
||||
<div class="poster_info">user%d</div>
|
||||
<div class="headerandpost">
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 := `<div class="inner">Hello<br>World<p>End</p></div>`
|
||||
posts, err := ParsePostsFromHTML(fmt.Sprintf(`<html><body><div class="post"><div class="inner">%s</div></div></body></html>`,
|
||||
posts, err := ParsePostsFromHTML(core.Sprintf(`<html><body><div class="post"><div class="inner">%s</div></div></body></html>`,
|
||||
"First text<br>Second text<div>Third text</div>"))
|
||||
// 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}},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(`<html><body></body></html>`))
|
||||
doc, err := html.Parse(core.NewReader(`<html><body></body></html>`))
|
||||
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(`<html><body><div class="paperentry"></div></body></html>`))
|
||||
doc, err := html.Parse(core.NewReader(`<html><body><div class="paperentry"></div></body></html>`))
|
||||
require.NoError(t, err)
|
||||
|
||||
papers := extractIACRPapers(doc)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
|
|
|
|||
15
git/git.go
15
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
|
|
|
|||
2
go.mod
2
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
|
||||
|
|
|
|||
4
go.sum
4
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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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++
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue