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