chore(ax): add usage docs to exported APIs
Some checks failed
Security Scan / security (push) Failing after 10s
Test / test (push) Successful in 2m11s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-30 14:11:15 +00:00
parent dd59b177c6
commit a0fac1341b
65 changed files with 298 additions and 0 deletions

View file

@ -26,6 +26,7 @@ type Spinner struct {
}
// NewSpinner creates a new Clotho orchestrator.
// Usage: NewSpinner(...)
func NewSpinner(cfg ClothoConfig, agents map[string]AgentConfig) *Spinner {
return &Spinner{
Config: cfg,
@ -35,6 +36,7 @@ func NewSpinner(cfg ClothoConfig, agents map[string]AgentConfig) *Spinner {
// DeterminePlan decides if a signal requires dual-run verification based on
// the global strategy, agent configuration, and repository criticality.
// Usage: DeterminePlan(...)
func (s *Spinner) DeterminePlan(signal *jobrunner.PipelineSignal, agentName string) RunMode {
if s.Config.Strategy != "clotho-verified" {
return ModeStandard
@ -57,6 +59,7 @@ func (s *Spinner) DeterminePlan(signal *jobrunner.PipelineSignal, agentName stri
}
// GetVerifierModel returns the model for the secondary "signed" verification run.
// Usage: GetVerifierModel(...)
func (s *Spinner) GetVerifierModel(agentName string) string {
agent, ok := s.Agents[agentName]
if !ok || agent.VerifyModel == "" {
@ -67,6 +70,7 @@ func (s *Spinner) GetVerifierModel(agentName string) string {
// FindByForgejoUser resolves a Forgejo username to the agent config key and config.
// This decouples agent naming (mythological roles) from Forgejo identity.
// Usage: FindByForgejoUser(...)
func (s *Spinner) FindByForgejoUser(forgejoUser string) (string, AgentConfig, bool) {
if forgejoUser == "" {
return "", AgentConfig{}, false
@ -86,6 +90,7 @@ func (s *Spinner) FindByForgejoUser(forgejoUser string) (string, AgentConfig, bo
// Weave compares primary and verifier outputs. Returns true if they converge.
// This is a placeholder for future semantic diff logic.
// Usage: Weave(...)
func (s *Spinner) Weave(ctx context.Context, primaryOutput, signedOutput []byte) (bool, error) {
return string(primaryOutput) == string(signedOutput), nil
}

View file

@ -33,6 +33,7 @@ type ClothoConfig struct {
// LoadAgents reads agent targets from config and returns a map of AgentConfig.
// Returns an empty map (not an error) if no agents are configured.
// Usage: LoadAgents(...)
func LoadAgents(cfg *config.Config) (map[string]AgentConfig, error) {
var agents map[string]AgentConfig
if err := cfg.Get("agentci.agents", &agents); err != nil {
@ -63,6 +64,7 @@ func LoadAgents(cfg *config.Config) (map[string]AgentConfig, error) {
}
// LoadActiveAgents returns only active agents.
// Usage: LoadActiveAgents(...)
func LoadActiveAgents(cfg *config.Config) (map[string]AgentConfig, error) {
all, err := LoadAgents(cfg)
if err != nil {
@ -79,6 +81,7 @@ func LoadActiveAgents(cfg *config.Config) (map[string]AgentConfig, error) {
// LoadClothoConfig loads the Clotho orchestrator settings.
// Returns sensible defaults if no config is present.
// Usage: LoadClothoConfig(...)
func LoadClothoConfig(cfg *config.Config) (ClothoConfig, error) {
var cc ClothoConfig
if err := cfg.Get("agentci.clotho", &cc); err != nil {
@ -97,6 +100,7 @@ func LoadClothoConfig(cfg *config.Config) (ClothoConfig, error) {
}
// SaveAgent writes an agent config entry to the config file.
// Usage: SaveAgent(...)
func SaveAgent(cfg *config.Config, name string, ac AgentConfig) error {
key := fmt.Sprintf("agentci.agents.%s", name)
data := map[string]any{
@ -125,6 +129,7 @@ func SaveAgent(cfg *config.Config, name string, ac AgentConfig) error {
}
// RemoveAgent removes an agent from the config file.
// Usage: RemoveAgent(...)
func RemoveAgent(cfg *config.Config, name string) error {
var agents map[string]AgentConfig
if err := cfg.Get("agentci.agents", &agents); err != nil {
@ -138,6 +143,7 @@ func RemoveAgent(cfg *config.Config, name string) error {
}
// ListAgents returns all configured agents (active and inactive).
// Usage: ListAgents(...)
func ListAgents(cfg *config.Config) (map[string]AgentConfig, error) {
var agents map[string]AgentConfig
if err := cfg.Get("agentci.agents", &agents); err != nil {

View file

@ -16,6 +16,7 @@ var safeNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]+$`)
// SanitizePath ensures a filename or directory name is safe and prevents path traversal.
// Returns the validated input unchanged.
// Usage: SanitizePath(...)
func SanitizePath(input string) (string, error) {
if input == "" {
return "", coreerr.E("agentci.SanitizePath", "path element is required", nil)
@ -33,11 +34,13 @@ func SanitizePath(input string) (string, error) {
}
// ValidatePathElement validates a single local path element and returns its safe form.
// Usage: ValidatePathElement(...)
func ValidatePathElement(input string) (string, error) {
return SanitizePath(input)
}
// ResolvePathWithinRoot resolves a validated path element beneath a root directory.
// Usage: ResolvePathWithinRoot(...)
func ResolvePathWithinRoot(root string, input string) (string, string, error) {
safeName, err := ValidatePathElement(input)
if err != nil {
@ -60,6 +63,7 @@ func ResolvePathWithinRoot(root string, input string) (string, string, error) {
}
// ValidateRemoteDir validates a remote directory path used over SSH.
// Usage: ValidateRemoteDir(...)
func ValidateRemoteDir(dir string) (string, error) {
if strings.TrimSpace(dir) == "" {
return "", coreerr.E("agentci.ValidateRemoteDir", "directory is required", nil)
@ -107,6 +111,7 @@ func ValidateRemoteDir(dir string) (string, error) {
}
// JoinRemotePath joins validated remote path elements using forward slashes.
// Usage: JoinRemotePath(...)
func JoinRemotePath(base string, parts ...string) (string, error) {
safeBase, err := ValidateRemoteDir(base)
if err != nil {
@ -133,11 +138,13 @@ func JoinRemotePath(base string, parts ...string) (string, error) {
// EscapeShellArg wraps a string in single quotes for safe remote shell insertion.
// Prefer exec.Command arguments over constructing shell strings where possible.
// Usage: EscapeShellArg(...)
func EscapeShellArg(arg string) string {
return "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
}
// SecureSSHCommand creates an SSH exec.Cmd with strict host key checking and batch mode.
// Usage: SecureSSHCommand(...)
func SecureSSHCommand(host string, remoteCmd string) *exec.Cmd {
return exec.Command("ssh",
"-o", "StrictHostKeyChecking=yes",
@ -149,6 +156,7 @@ func SecureSSHCommand(host string, remoteCmd string) *exec.Cmd {
}
// MaskToken returns a masked version of a token for safe logging.
// Usage: MaskToken(...)
func MaskToken(token string) string {
if len(token) < 8 {
return "*****"

View file

@ -30,6 +30,7 @@ var (
)
// AddCollectCommands registers the 'collect' command and all subcommands.
// Usage: AddCollectCommands(...)
func AddCollectCommands(root *cli.Command) {
collectCmd := &cli.Command{
Use: "collect",

View file

@ -35,6 +35,7 @@ var (
)
// AddForgeCommands registers the 'forge' command and all subcommands.
// Usage: AddForgeCommands(...)
func AddForgeCommands(root *cli.Command) {
forgeCmd := &cli.Command{
Use: "forge",

View file

@ -32,6 +32,7 @@ var (
)
// AddGiteaCommands registers the 'gitea' command and all subcommands.
// Usage: AddGiteaCommands(...)
func AddGiteaCommands(root *cli.Command) {
giteaCmd := &cli.Command{
Use: "gitea",

View file

@ -27,6 +27,7 @@ var (
)
// AddScmCommands registers the 'scm' command and all subcommands.
// Usage: AddScmCommands(...)
func AddScmCommands(root *cli.Command) {
scmCmd := &cli.Command{
Use: "scm",

View file

@ -35,6 +35,7 @@ type BitcoinTalkCollector struct {
}
// Name returns the collector name.
// Usage: Name(...)
func (b *BitcoinTalkCollector) Name() string {
id := b.TopicID
if id == "" && b.URL != "" {
@ -44,6 +45,7 @@ func (b *BitcoinTalkCollector) Name() string {
}
// Collect gathers posts from a BitcoinTalk topic.
// Usage: Collect(...)
func (b *BitcoinTalkCollector) Collect(ctx context.Context, cfg *Config) (*Result, error) {
result := &Result{Source: b.Name()}
@ -283,6 +285,7 @@ func formatPostMarkdown(num int, post btPost) string {
// ParsePostsFromHTML parses BitcoinTalk posts from raw HTML content.
// This is exported for testing purposes.
// Usage: ParsePostsFromHTML(...)
func ParsePostsFromHTML(htmlContent string) ([]btPost, error) {
doc, err := html.Parse(strings.NewReader(htmlContent))
if err != nil {
@ -292,6 +295,7 @@ func ParsePostsFromHTML(htmlContent string) ([]btPost, error) {
}
// FormatPostMarkdown is exported for testing purposes.
// Usage: FormatPostMarkdown(...)
func FormatPostMarkdown(num int, author, date, content string) string {
return formatPostMarkdown(num, btPost{Author: author, Date: date, Content: content})
}
@ -307,6 +311,7 @@ type BitcoinTalkCollectorWithFetcher struct {
// SetHTTPClient replaces the package-level HTTP client.
// Use this in tests to inject a custom transport or timeout.
// Usage: SetHTTPClient(...)
func SetHTTPClient(c *http.Client) {
httpClient = c
}

View file

@ -67,6 +67,7 @@ type Result struct {
// NewConfig creates a Config with sensible defaults.
// It initialises a MockMedium for output if none is provided,
// sets up a rate limiter, state tracker, and event dispatcher.
// Usage: NewConfig(...)
func NewConfig(outputDir string) *Config {
m := io.NewMockMedium()
return &Config{
@ -79,6 +80,7 @@ func NewConfig(outputDir string) *Config {
}
// NewConfigWithMedium creates a Config using the specified storage medium.
// Usage: NewConfigWithMedium(...)
func NewConfigWithMedium(m io.Medium, outputDir string) *Config {
return &Config{
Output: m,
@ -90,6 +92,7 @@ func NewConfigWithMedium(m io.Medium, outputDir string) *Config {
}
// MergeResults combines multiple results into a single aggregated result.
// Usage: MergeResults(...)
func MergeResults(source string, results ...*Result) *Result {
merged := &Result{Source: source}
for _, r := range results {

View file

@ -59,6 +59,7 @@ type Dispatcher struct {
}
// NewDispatcher creates a new event dispatcher.
// Usage: NewDispatcher(...)
func NewDispatcher() *Dispatcher {
return &Dispatcher{
handlers: make(map[string][]EventHandler),
@ -67,6 +68,7 @@ func NewDispatcher() *Dispatcher {
// On registers a handler for an event type. Multiple handlers can be
// registered for the same event type and will be called in order.
// Usage: On(...)
func (d *Dispatcher) On(eventType string, handler EventHandler) {
d.mu.Lock()
defer d.mu.Unlock()
@ -76,6 +78,7 @@ func (d *Dispatcher) On(eventType string, handler EventHandler) {
// Emit dispatches an event to all registered handlers for that event type.
// If no handlers are registered for the event type, the event is silently dropped.
// The event's Time field is set to now if it is zero.
// Usage: Emit(...)
func (d *Dispatcher) Emit(event Event) {
if event.Time.IsZero() {
event.Time = time.Now()
@ -91,6 +94,7 @@ func (d *Dispatcher) Emit(event Event) {
}
// EmitStart emits a start event for the given source.
// Usage: EmitStart(...)
func (d *Dispatcher) EmitStart(source, message string) {
d.Emit(Event{
Type: EventStart,
@ -100,6 +104,7 @@ func (d *Dispatcher) EmitStart(source, message string) {
}
// EmitProgress emits a progress event.
// Usage: EmitProgress(...)
func (d *Dispatcher) EmitProgress(source, message string, data any) {
d.Emit(Event{
Type: EventProgress,
@ -110,6 +115,7 @@ func (d *Dispatcher) EmitProgress(source, message string, data any) {
}
// EmitItem emits an item event.
// Usage: EmitItem(...)
func (d *Dispatcher) EmitItem(source, message string, data any) {
d.Emit(Event{
Type: EventItem,
@ -120,6 +126,7 @@ func (d *Dispatcher) EmitItem(source, message string, data any) {
}
// EmitError emits an error event.
// Usage: EmitError(...)
func (d *Dispatcher) EmitError(source, message string, data any) {
d.Emit(Event{
Type: EventError,
@ -130,6 +137,7 @@ func (d *Dispatcher) EmitError(source, message string, data any) {
}
// EmitComplete emits a complete event.
// Usage: EmitComplete(...)
func (d *Dispatcher) EmitComplete(source, message string, data any) {
d.Emit(Event{
Type: EventComplete,

View file

@ -25,12 +25,14 @@ type Excavator struct {
}
// Name returns the orchestrator name.
// Usage: Name(...)
func (e *Excavator) Name() string {
return "excavator"
}
// Run executes all collectors sequentially, respecting rate limits and
// using state for resume support. Results are aggregated from all collectors.
// Usage: Run(...)
func (e *Excavator) Run(ctx context.Context, cfg *Config) (*Result, error) {
result := &Result{Source: e.Name()}

View file

@ -55,6 +55,7 @@ type GitHubCollector struct {
}
// Name returns the collector name.
// Usage: Name(...)
func (g *GitHubCollector) Name() string {
if g.Repo != "" {
return fmt.Sprintf("github:%s/%s", g.Org, g.Repo)
@ -63,6 +64,7 @@ func (g *GitHubCollector) Name() string {
}
// Collect gathers issues and/or PRs from GitHub repositories.
// Usage: Collect(...)
func (g *GitHubCollector) Collect(ctx context.Context, cfg *Config) (*Result, error) {
result := &Result{Source: g.Name()}

View file

@ -31,6 +31,7 @@ type MarketCollector struct {
}
// Name returns the collector name.
// Usage: Name(...)
func (m *MarketCollector) Name() string {
return fmt.Sprintf("market:%s", m.CoinID)
}
@ -65,6 +66,7 @@ type historicalData struct {
}
// Collect gathers market data from CoinGecko.
// Usage: Collect(...)
func (m *MarketCollector) Collect(ctx context.Context, cfg *Config) (*Result, error) {
result := &Result{Source: m.Name()}
@ -274,6 +276,7 @@ func formatMarketSummary(data *coinData) string {
}
// FormatMarketSummary is exported for testing.
// Usage: FormatMarketSummary(...)
func FormatMarketSummary(data *coinData) string {
return formatMarketSummary(data)
}

View file

@ -39,6 +39,7 @@ type PapersCollector struct {
}
// Name returns the collector name.
// Usage: Name(...)
func (p *PapersCollector) Name() string {
return fmt.Sprintf("papers:%s", p.Source)
}
@ -55,6 +56,7 @@ type paper struct {
}
// Collect gathers papers from the configured sources.
// Usage: Collect(...)
func (p *PapersCollector) Collect(ctx context.Context, cfg *Config) (*Result, error) {
result := &Result{Source: p.Name()}
@ -408,6 +410,7 @@ func formatPaperMarkdown(ppr paper) string {
}
// FormatPaperMarkdown is exported for testing.
// Usage: FormatPaperMarkdown(...)
func FormatPaperMarkdown(title string, authors []string, date, paperURL, source, abstract string) string {
return formatPaperMarkdown(paper{
Title: title,

View file

@ -25,12 +25,14 @@ type Processor struct {
}
// Name returns the processor name.
// Usage: Name(...)
func (p *Processor) Name() string {
return fmt.Sprintf("process:%s", p.Source)
}
// Process reads files from the source directory, converts HTML or JSON
// to clean markdown, and writes the results to the output directory.
// Usage: Process(...)
func (p *Processor) Process(ctx context.Context, cfg *Config) (*Result, error) {
result := &Result{Source: p.Name()}
@ -333,11 +335,13 @@ func jsonValueToMarkdown(b *strings.Builder, data any, depth int) {
}
// HTMLToMarkdown is exported for testing.
// Usage: HTMLToMarkdown(...)
func HTMLToMarkdown(content string) (string, error) {
return htmlToMarkdown(content)
}
// JSONToMarkdown is exported for testing.
// Usage: JSONToMarkdown(...)
func JSONToMarkdown(content string) (string, error) {
return jsonToMarkdown(content)
}

View file

@ -32,6 +32,7 @@ var defaultDelays = map[string]time.Duration{
}
// NewRateLimiter creates a limiter with default delays.
// Usage: NewRateLimiter(...)
func NewRateLimiter() *RateLimiter {
delays := make(map[string]time.Duration, len(defaultDelays))
maps.Copy(delays, defaultDelays)

View file

@ -41,6 +41,7 @@ type StateEntry struct {
// NewState creates a state tracker that persists to the given path
// using the provided storage medium.
// Usage: NewState(...)
func NewState(m io.Medium, path string) *State {
return &State{
medium: m,
@ -51,6 +52,7 @@ func NewState(m io.Medium, path string) *State {
// Load reads state from disk. If the file does not exist, the state
// is initialised as empty without error.
// Usage: Load(...)
func (s *State) Load() error {
s.mu.Lock()
defer s.mu.Unlock()
@ -77,6 +79,7 @@ func (s *State) Load() error {
}
// Save writes state to disk.
// Usage: Save(...)
func (s *State) Save() error {
s.mu.Lock()
defer s.mu.Unlock()
@ -95,6 +98,7 @@ func (s *State) Save() error {
// Get returns a copy of the state for a source. The second return value
// indicates whether the entry was found.
// Usage: Get(...)
func (s *State) Get(source string) (*StateEntry, bool) {
s.mu.Lock()
defer s.mu.Unlock()
@ -108,6 +112,7 @@ func (s *State) Get(source string) (*StateEntry, bool) {
}
// Set updates state for a source.
// Usage: Set(...)
func (s *State) Set(source string, entry *StateEntry) {
s.mu.Lock()
defer s.mu.Unlock()

View file

@ -24,6 +24,7 @@ type Client struct {
}
// New creates a new Forgejo API client for the given URL and token.
// Usage: New(...)
func New(url, token string) (*Client, error) {
api, err := forgejo.NewClient(url, forgejo.SetToken(token))
if err != nil {

View file

@ -27,6 +27,8 @@ const (
// 1. ~/.core/config.yaml keys: forge.token, forge.url
// 2. FORGE_TOKEN + FORGE_URL environment variables (override config file)
// 3. Provided flag overrides (highest priority; pass empty to skip)
//
// Usage: NewFromConfig(...)
func NewFromConfig(flagURL, flagToken string) (*Client, error) {
url, token, err := ResolveConfig(flagURL, flagToken)
if err != nil {
@ -42,6 +44,7 @@ func NewFromConfig(flagURL, flagToken string) (*Client, error) {
// ResolveConfig resolves the Forgejo URL and token from all config sources.
// Flag values take highest priority, then env vars, then config file.
// Usage: ResolveConfig(...)
func ResolveConfig(flagURL, flagToken string) (url, token string, err error) {
// Start with config file values
cfg, cfgErr := config.New()
@ -75,6 +78,7 @@ func ResolveConfig(flagURL, flagToken string) (url, token string, err error) {
}
// SaveConfig persists the Forgejo URL and/or token to the config file.
// Usage: SaveConfig(...)
func SaveConfig(url, token string) error {
cfg, err := config.New()
if err != nil {

View file

@ -19,6 +19,7 @@ type ListIssuesOpts struct {
}
// ListIssues returns issues for the given repository.
// Usage: ListIssues(...)
func (c *Client) ListIssues(owner, repo string, opts ListIssuesOpts) ([]*forgejo.Issue, error) {
state := forgejo.StateOpen
switch opts.State {
@ -54,6 +55,7 @@ func (c *Client) ListIssues(owner, repo string, opts ListIssuesOpts) ([]*forgejo
}
// GetIssue returns a single issue by number.
// Usage: GetIssue(...)
func (c *Client) GetIssue(owner, repo string, number int64) (*forgejo.Issue, error) {
issue, _, err := c.api.GetIssue(owner, repo, number)
if err != nil {
@ -64,6 +66,7 @@ func (c *Client) GetIssue(owner, repo string, number int64) (*forgejo.Issue, err
}
// CreateIssue creates a new issue in the given repository.
// Usage: CreateIssue(...)
func (c *Client) CreateIssue(owner, repo string, opts forgejo.CreateIssueOption) (*forgejo.Issue, error) {
issue, _, err := c.api.CreateIssue(owner, repo, opts)
if err != nil {
@ -74,6 +77,7 @@ func (c *Client) CreateIssue(owner, repo string, opts forgejo.CreateIssueOption)
}
// EditIssue edits an existing issue.
// Usage: EditIssue(...)
func (c *Client) EditIssue(owner, repo string, number int64, opts forgejo.EditIssueOption) (*forgejo.Issue, error) {
issue, _, err := c.api.EditIssue(owner, repo, number, opts)
if err != nil {
@ -84,6 +88,7 @@ func (c *Client) EditIssue(owner, repo string, number int64, opts forgejo.EditIs
}
// AssignIssue assigns an issue to the specified users.
// Usage: AssignIssue(...)
func (c *Client) AssignIssue(owner, repo string, number int64, assignees []string) error {
_, _, err := c.api.EditIssue(owner, repo, number, forgejo.EditIssueOption{
Assignees: assignees,
@ -95,6 +100,7 @@ func (c *Client) AssignIssue(owner, repo string, number int64, assignees []strin
}
// ListPullRequests returns pull requests for the given repository.
// Usage: ListPullRequests(...)
func (c *Client) ListPullRequests(owner, repo string, state string) ([]*forgejo.PullRequest, error) {
st := forgejo.StateOpen
switch state {
@ -128,6 +134,7 @@ func (c *Client) ListPullRequests(owner, repo string, state string) ([]*forgejo.
}
// ListPullRequestsIter returns an iterator over pull requests for the given repository.
// Usage: ListPullRequestsIter(...)
func (c *Client) ListPullRequestsIter(owner, repo string, state string) iter.Seq2[*forgejo.PullRequest, error] {
st := forgejo.StateOpen
switch state {
@ -162,6 +169,7 @@ func (c *Client) ListPullRequestsIter(owner, repo string, state string) iter.Seq
}
// GetPullRequest returns a single pull request by number.
// Usage: GetPullRequest(...)
func (c *Client) GetPullRequest(owner, repo string, number int64) (*forgejo.PullRequest, error) {
pr, _, err := c.api.GetPullRequest(owner, repo, number)
if err != nil {
@ -172,6 +180,7 @@ func (c *Client) GetPullRequest(owner, repo string, number int64) (*forgejo.Pull
}
// CreateIssueComment posts a comment on an issue or pull request.
// Usage: CreateIssueComment(...)
func (c *Client) CreateIssueComment(owner, repo string, issue int64, body string) error {
_, _, err := c.api.CreateIssueComment(owner, repo, issue, forgejo.CreateIssueCommentOption{
Body: body,
@ -183,6 +192,7 @@ func (c *Client) CreateIssueComment(owner, repo string, issue int64, body string
}
// ListIssueComments returns comments for an issue.
// Usage: ListIssueComments(...)
func (c *Client) ListIssueComments(owner, repo string, number int64) ([]*forgejo.Comment, error) {
var all []*forgejo.Comment
page := 1
@ -207,6 +217,7 @@ func (c *Client) ListIssueComments(owner, repo string, number int64) ([]*forgejo
}
// CloseIssue closes an issue by setting its state to closed.
// Usage: CloseIssue(...)
func (c *Client) CloseIssue(owner, repo string, number int64) error {
closed := forgejo.StateClosed
_, _, err := c.api.EditIssue(owner, repo, number, forgejo.EditIssueOption{

View file

@ -14,6 +14,7 @@ import (
// Note: The Forgejo SDK does not have a dedicated org-level labels endpoint.
// This lists labels from the first repo found, which works when orgs use shared label sets.
// For org-wide label management, use ListRepoLabels with a specific repo.
// Usage: ListOrgLabels(...)
func (c *Client) ListOrgLabels(org string) ([]*forgejo.Label, error) {
// Forgejo doesn't expose org-level labels via SDK — list repos and aggregate unique labels.
repos, err := c.ListOrgRepos(org)
@ -30,6 +31,7 @@ func (c *Client) ListOrgLabels(org string) ([]*forgejo.Label, error) {
}
// ListRepoLabels returns all labels for a repository.
// Usage: ListRepoLabels(...)
func (c *Client) ListRepoLabels(owner, repo string) ([]*forgejo.Label, error) {
var all []*forgejo.Label
page := 1
@ -54,6 +56,7 @@ func (c *Client) ListRepoLabels(owner, repo string) ([]*forgejo.Label, error) {
}
// CreateRepoLabel creates a label on a repository.
// Usage: CreateRepoLabel(...)
func (c *Client) CreateRepoLabel(owner, repo string, opts forgejo.CreateLabelOption) (*forgejo.Label, error) {
label, _, err := c.api.CreateLabel(owner, repo, opts)
if err != nil {
@ -64,6 +67,7 @@ func (c *Client) CreateRepoLabel(owner, repo string, opts forgejo.CreateLabelOpt
}
// GetLabelByName retrieves a specific label by name from a repository.
// Usage: GetLabelByName(...)
func (c *Client) GetLabelByName(owner, repo, name string) (*forgejo.Label, error) {
labels, err := c.ListRepoLabels(owner, repo)
if err != nil {
@ -80,6 +84,7 @@ func (c *Client) GetLabelByName(owner, repo, name string) (*forgejo.Label, error
}
// EnsureLabel checks if a label exists, and creates it if it doesn't.
// Usage: EnsureLabel(...)
func (c *Client) EnsureLabel(owner, repo, name, color string) (*forgejo.Label, error) {
label, err := c.GetLabelByName(owner, repo, name)
if err == nil {
@ -93,6 +98,7 @@ func (c *Client) EnsureLabel(owner, repo, name, color string) (*forgejo.Label, e
}
// AddIssueLabels adds labels to an issue.
// Usage: AddIssueLabels(...)
func (c *Client) AddIssueLabels(owner, repo string, number int64, labelIDs []int64) error {
_, _, err := c.api.AddIssueLabels(owner, repo, number, forgejo.IssueLabelsOption{
Labels: labelIDs,
@ -104,6 +110,7 @@ func (c *Client) AddIssueLabels(owner, repo string, number int64, labelIDs []int
}
// RemoveIssueLabel removes a label from an issue.
// Usage: RemoveIssueLabel(...)
func (c *Client) RemoveIssueLabel(owner, repo string, number int64, labelID int64) error {
_, err := c.api.DeleteIssueLabel(owner, repo, number, labelID)
if err != nil {

View file

@ -9,6 +9,7 @@ import (
)
// ListMyOrgs returns all organisations for the authenticated user.
// Usage: ListMyOrgs(...)
func (c *Client) ListMyOrgs() ([]*forgejo.Organization, error) {
var all []*forgejo.Organization
page := 1
@ -33,6 +34,7 @@ func (c *Client) ListMyOrgs() ([]*forgejo.Organization, error) {
}
// GetOrg returns a single organisation by name.
// Usage: GetOrg(...)
func (c *Client) GetOrg(name string) (*forgejo.Organization, error) {
org, _, err := c.api.GetOrg(name)
if err != nil {
@ -43,6 +45,7 @@ func (c *Client) GetOrg(name string) (*forgejo.Organization, error) {
}
// CreateOrg creates a new organisation.
// Usage: CreateOrg(...)
func (c *Client) CreateOrg(opts forgejo.CreateOrgOption) (*forgejo.Organization, error) {
org, _, err := c.api.CreateOrg(opts)
if err != nil {

View file

@ -17,6 +17,7 @@ import (
)
// MergePullRequest merges a pull request with the given method ("squash", "rebase", "merge").
// Usage: MergePullRequest(...)
func (c *Client) MergePullRequest(owner, repo string, index int64, method string) error {
style := forgejo.MergeStyleMerge
switch method {
@ -42,6 +43,7 @@ func (c *Client) MergePullRequest(owner, repo string, index int64, method string
// SetPRDraft sets or clears the draft status on a pull request.
// The Forgejo SDK v2.2.0 doesn't expose the draft field on EditPullRequestOption,
// so we use a raw HTTP PATCH request.
// Usage: SetPRDraft(...)
func (c *Client) SetPRDraft(owner, repo string, index int64, draft bool) error {
safeOwner, err := agentci.ValidatePathElement(owner)
if err != nil {
@ -83,6 +85,7 @@ func (c *Client) SetPRDraft(owner, repo string, index int64, draft bool) error {
}
// ListPRReviews returns all reviews for a pull request.
// Usage: ListPRReviews(...)
func (c *Client) ListPRReviews(owner, repo string, index int64) ([]*forgejo.PullReview, error) {
var all []*forgejo.PullReview
page := 1
@ -107,6 +110,7 @@ func (c *Client) ListPRReviews(owner, repo string, index int64) ([]*forgejo.Pull
}
// GetCombinedStatus returns the combined commit status for a ref (SHA or branch).
// Usage: GetCombinedStatus(...)
func (c *Client) GetCombinedStatus(owner, repo string, ref string) (*forgejo.CombinedStatus, error) {
status, _, err := c.api.GetCombinedStatus(owner, repo, ref)
if err != nil {
@ -116,6 +120,7 @@ func (c *Client) GetCombinedStatus(owner, repo string, ref string) (*forgejo.Com
}
// DismissReview dismisses a pull request review by ID.
// Usage: DismissReview(...)
func (c *Client) DismissReview(owner, repo string, index, reviewID int64, message string) error {
_, err := c.api.DismissPullReview(owner, repo, index, reviewID, forgejo.DismissPullReviewOptions{
Message: message,

View file

@ -11,6 +11,7 @@ import (
)
// ListOrgRepos returns all repositories for the given organisation.
// Usage: ListOrgRepos(...)
func (c *Client) ListOrgRepos(org string) ([]*forgejo.Repository, error) {
var all []*forgejo.Repository
page := 1
@ -35,6 +36,7 @@ func (c *Client) ListOrgRepos(org string) ([]*forgejo.Repository, error) {
}
// ListOrgReposIter returns an iterator over repositories for the given organisation.
// Usage: ListOrgReposIter(...)
func (c *Client) ListOrgReposIter(org string) iter.Seq2[*forgejo.Repository, error] {
return func(yield func(*forgejo.Repository, error) bool) {
page := 1
@ -60,6 +62,7 @@ func (c *Client) ListOrgReposIter(org string) iter.Seq2[*forgejo.Repository, err
}
// ListUserRepos returns all repositories for the authenticated user.
// Usage: ListUserRepos(...)
func (c *Client) ListUserRepos() ([]*forgejo.Repository, error) {
var all []*forgejo.Repository
page := 1
@ -84,6 +87,7 @@ func (c *Client) ListUserRepos() ([]*forgejo.Repository, error) {
}
// ListUserReposIter returns an iterator over repositories for the authenticated user.
// Usage: ListUserReposIter(...)
func (c *Client) ListUserReposIter() iter.Seq2[*forgejo.Repository, error] {
return func(yield func(*forgejo.Repository, error) bool) {
page := 1
@ -109,6 +113,7 @@ func (c *Client) ListUserReposIter() iter.Seq2[*forgejo.Repository, error] {
}
// GetRepo returns a single repository by owner and name.
// Usage: GetRepo(...)
func (c *Client) GetRepo(owner, name string) (*forgejo.Repository, error) {
repo, _, err := c.api.GetRepo(owner, name)
if err != nil {
@ -119,6 +124,7 @@ func (c *Client) GetRepo(owner, name string) (*forgejo.Repository, error) {
}
// CreateOrgRepo creates a new empty repository under an organisation.
// Usage: CreateOrgRepo(...)
func (c *Client) CreateOrgRepo(org string, opts forgejo.CreateRepoOption) (*forgejo.Repository, error) {
repo, _, err := c.api.CreateOrgRepo(org, opts)
if err != nil {
@ -129,6 +135,7 @@ func (c *Client) CreateOrgRepo(org string, opts forgejo.CreateRepoOption) (*forg
}
// DeleteRepo deletes a repository from Forgejo.
// Usage: DeleteRepo(...)
func (c *Client) DeleteRepo(owner, name string) error {
_, err := c.api.DeleteRepo(owner, name)
if err != nil {
@ -140,6 +147,7 @@ func (c *Client) DeleteRepo(owner, name string) error {
// MigrateRepo migrates a repository from an external service using the Forgejo migration API.
// Unlike CreateMirror, this supports importing issues, labels, PRs, and more.
// Usage: MigrateRepo(...)
func (c *Client) MigrateRepo(opts forgejo.MigrateRepoOption) (*forgejo.Repository, error) {
repo, _, err := c.api.MigrateRepo(opts)
if err != nil {

View file

@ -9,6 +9,7 @@ import (
)
// CreateRepoWebhook creates a webhook on a repository.
// Usage: CreateRepoWebhook(...)
func (c *Client) CreateRepoWebhook(owner, repo string, opts forgejo.CreateHookOption) (*forgejo.Hook, error) {
hook, _, err := c.api.CreateRepoHook(owner, repo, opts)
if err != nil {
@ -19,6 +20,7 @@ func (c *Client) CreateRepoWebhook(owner, repo string, opts forgejo.CreateHookOp
}
// ListRepoWebhooks returns all webhooks for a repository.
// Usage: ListRepoWebhooks(...)
func (c *Client) ListRepoWebhooks(owner, repo string) ([]*forgejo.Hook, error) {
var all []*forgejo.Hook
page := 1

View file

@ -30,16 +30,19 @@ type RepoStatus struct {
}
// IsDirty returns true if there are uncommitted changes.
// Usage: IsDirty(...)
func (s *RepoStatus) IsDirty() bool {
return s.Modified > 0 || s.Untracked > 0 || s.Staged > 0
}
// HasUnpushed returns true if there are commits to push.
// Usage: HasUnpushed(...)
func (s *RepoStatus) HasUnpushed() bool {
return s.Ahead > 0
}
// HasUnpulled returns true if there are commits to pull.
// Usage: HasUnpulled(...)
func (s *RepoStatus) HasUnpulled() bool {
return s.Behind > 0
}
@ -53,6 +56,7 @@ type StatusOptions struct {
}
// Status checks git status for multiple repositories in parallel.
// Usage: Status(...)
func Status(ctx context.Context, opts StatusOptions) []RepoStatus {
var wg sync.WaitGroup
results := make([]RepoStatus, len(opts.Paths))
@ -74,6 +78,7 @@ func Status(ctx context.Context, opts StatusOptions) []RepoStatus {
}
// StatusIter returns an iterator over git status for multiple repositories.
// Usage: StatusIter(...)
func StatusIter(ctx context.Context, opts StatusOptions) iter.Seq[RepoStatus] {
return func(yield func(RepoStatus) bool) {
results := Status(ctx, opts)
@ -158,17 +163,20 @@ func getAheadBehind(ctx context.Context, path string) (ahead, behind int) {
// Push pushes commits for a single repository.
// Uses interactive mode to support SSH passphrase prompts.
// Usage: Push(...)
func Push(ctx context.Context, path string) error {
return gitInteractive(ctx, path, "push")
}
// Pull pulls changes for a single repository.
// Uses interactive mode to support SSH passphrase prompts.
// Usage: Pull(...)
func Pull(ctx context.Context, path string) error {
return gitInteractive(ctx, path, "pull", "--rebase")
}
// IsNonFastForward checks if an error is a non-fast-forward rejection.
// Usage: IsNonFastForward(...)
func IsNonFastForward(err error) bool {
if err == nil {
return false
@ -212,11 +220,13 @@ type PushResult struct {
// PushMultiple pushes multiple repositories sequentially.
// Sequential because SSH passphrase prompts need user interaction.
// Usage: PushMultiple(...)
func PushMultiple(ctx context.Context, paths []string, names map[string]string) []PushResult {
return slices.Collect(PushMultipleIter(ctx, paths, names))
}
// PushMultipleIter returns an iterator that pushes repositories sequentially and yields results.
// Usage: PushMultipleIter(...)
func PushMultipleIter(ctx context.Context, paths []string, names map[string]string) iter.Seq[PushResult] {
return func(yield func(PushResult) bool) {
for _, path := range paths {
@ -271,6 +281,7 @@ type GitError struct {
}
// Error returns the git error message, preferring stderr output.
// Usage: Error(...)
func (e *GitError) Error() string {
// Return just the stderr message, trimmed
msg := strings.TrimSpace(e.Stderr)
@ -281,6 +292,7 @@ func (e *GitError) Error() string {
}
// Unwrap returns the underlying error for error chain inspection.
// Usage: Unwrap(...)
func (e *GitError) Unwrap() error {
return e.Err
}

View file

@ -56,6 +56,7 @@ type Service struct {
}
// NewService creates a git service factory.
// Usage: NewService(...)
func NewService(opts ServiceOptions) func(*core.Core) (any, error) {
return func(c *core.Core) (any, error) {
return &Service{
@ -65,6 +66,7 @@ func NewService(opts ServiceOptions) func(*core.Core) (any, error) {
}
// OnStartup registers query and task handlers.
// Usage: OnStartup(...)
func (s *Service) OnStartup(ctx context.Context) error {
s.Core().RegisterQuery(s.handleQuery)
s.Core().RegisterTask(s.handleTask)
@ -103,14 +105,17 @@ func (s *Service) handleTask(c *core.Core, t core.Task) core.Result {
}
// Status returns last status result.
// Usage: Status(...)
func (s *Service) Status() []RepoStatus { return s.lastStatus }
// StatusIter returns an iterator over last status result.
// Usage: StatusIter(...)
func (s *Service) StatusIter() iter.Seq[RepoStatus] {
return slices.Values(s.lastStatus)
}
// DirtyRepos returns repos with uncommitted changes.
// Usage: DirtyRepos(...)
func (s *Service) DirtyRepos() []RepoStatus {
var dirty []RepoStatus
for _, st := range s.lastStatus {
@ -122,6 +127,7 @@ func (s *Service) DirtyRepos() []RepoStatus {
}
// DirtyReposIter returns an iterator over repos with uncommitted changes.
// Usage: DirtyReposIter(...)
func (s *Service) DirtyReposIter() iter.Seq[RepoStatus] {
return func(yield func(RepoStatus) bool) {
for _, st := range s.lastStatus {
@ -135,6 +141,7 @@ func (s *Service) DirtyReposIter() iter.Seq[RepoStatus] {
}
// AheadRepos returns repos with unpushed commits.
// Usage: AheadRepos(...)
func (s *Service) AheadRepos() []RepoStatus {
var ahead []RepoStatus
for _, st := range s.lastStatus {
@ -146,6 +153,7 @@ func (s *Service) AheadRepos() []RepoStatus {
}
// AheadReposIter returns an iterator over repos with unpushed commits.
// Usage: AheadReposIter(...)
func (s *Service) AheadReposIter() iter.Seq[RepoStatus] {
return func(yield func(RepoStatus) bool) {
for _, st := range s.lastStatus {

View file

@ -23,6 +23,7 @@ type Client struct {
}
// New creates a new Gitea API client for the given URL and token.
// Usage: New(...)
func New(url, token string) (*Client, error) {
api, err := gitea.NewClient(url, gitea.SetToken(token))
if err != nil {

View file

@ -27,6 +27,8 @@ const (
// 1. ~/.core/config.yaml keys: gitea.token, gitea.url
// 2. GITEA_TOKEN + GITEA_URL environment variables (override config file)
// 3. Provided flag overrides (highest priority; pass empty to skip)
//
// Usage: NewFromConfig(...)
func NewFromConfig(flagURL, flagToken string) (*Client, error) {
url, token, err := ResolveConfig(flagURL, flagToken)
if err != nil {
@ -42,6 +44,7 @@ func NewFromConfig(flagURL, flagToken string) (*Client, error) {
// ResolveConfig resolves the Gitea URL and token from all config sources.
// Flag values take highest priority, then env vars, then config file.
// Usage: ResolveConfig(...)
func ResolveConfig(flagURL, flagToken string) (url, token string, err error) {
// Start with config file values
cfg, cfgErr := config.New()
@ -75,6 +78,7 @@ func ResolveConfig(flagURL, flagToken string) (url, token string, err error) {
}
// SaveConfig persists the Gitea URL and/or token to the config file.
// Usage: SaveConfig(...)
func SaveConfig(url, token string) error {
cfg, err := config.New()
if err != nil {

View file

@ -18,6 +18,7 @@ type ListIssuesOpts struct {
}
// ListIssues returns issues for the given repository.
// Usage: ListIssues(...)
func (c *Client) ListIssues(owner, repo string, opts ListIssuesOpts) ([]*gitea.Issue, error) {
state := gitea.StateOpen
switch opts.State {
@ -50,6 +51,7 @@ func (c *Client) ListIssues(owner, repo string, opts ListIssuesOpts) ([]*gitea.I
}
// GetIssue returns a single issue by number.
// Usage: GetIssue(...)
func (c *Client) GetIssue(owner, repo string, number int64) (*gitea.Issue, error) {
issue, _, err := c.api.GetIssue(owner, repo, number)
if err != nil {
@ -60,6 +62,7 @@ func (c *Client) GetIssue(owner, repo string, number int64) (*gitea.Issue, error
}
// CreateIssue creates a new issue in the given repository.
// Usage: CreateIssue(...)
func (c *Client) CreateIssue(owner, repo string, opts gitea.CreateIssueOption) (*gitea.Issue, error) {
issue, _, err := c.api.CreateIssue(owner, repo, opts)
if err != nil {
@ -70,6 +73,7 @@ func (c *Client) CreateIssue(owner, repo string, opts gitea.CreateIssueOption) (
}
// ListPullRequests returns pull requests for the given repository.
// Usage: ListPullRequests(...)
func (c *Client) ListPullRequests(owner, repo string, state string) ([]*gitea.PullRequest, error) {
st := gitea.StateOpen
switch state {
@ -103,6 +107,7 @@ func (c *Client) ListPullRequests(owner, repo string, state string) ([]*gitea.Pu
}
// ListPullRequestsIter returns an iterator over pull requests for the given repository.
// Usage: ListPullRequestsIter(...)
func (c *Client) ListPullRequestsIter(owner, repo string, state string) iter.Seq2[*gitea.PullRequest, error] {
st := gitea.StateOpen
switch state {
@ -137,6 +142,7 @@ func (c *Client) ListPullRequestsIter(owner, repo string, state string) iter.Seq
}
// GetPullRequest returns a single pull request by number.
// Usage: GetPullRequest(...)
func (c *Client) GetPullRequest(owner, repo string, number int64) (*gitea.PullRequest, error) {
pr, _, err := c.api.GetPullRequest(owner, repo, number)
if err != nil {

View file

@ -11,6 +11,7 @@ import (
)
// ListOrgRepos returns all repositories for the given organisation.
// Usage: ListOrgRepos(...)
func (c *Client) ListOrgRepos(org string) ([]*gitea.Repository, error) {
var all []*gitea.Repository
page := 1
@ -35,6 +36,7 @@ func (c *Client) ListOrgRepos(org string) ([]*gitea.Repository, error) {
}
// ListOrgReposIter returns an iterator over repositories for the given organisation.
// Usage: ListOrgReposIter(...)
func (c *Client) ListOrgReposIter(org string) iter.Seq2[*gitea.Repository, error] {
return func(yield func(*gitea.Repository, error) bool) {
page := 1
@ -60,6 +62,7 @@ func (c *Client) ListOrgReposIter(org string) iter.Seq2[*gitea.Repository, error
}
// ListUserRepos returns all repositories for the authenticated user.
// Usage: ListUserRepos(...)
func (c *Client) ListUserRepos() ([]*gitea.Repository, error) {
var all []*gitea.Repository
page := 1
@ -84,6 +87,7 @@ func (c *Client) ListUserRepos() ([]*gitea.Repository, error) {
}
// ListUserReposIter returns an iterator over repositories for the authenticated user.
// Usage: ListUserReposIter(...)
func (c *Client) ListUserReposIter() iter.Seq2[*gitea.Repository, error] {
return func(yield func(*gitea.Repository, error) bool) {
page := 1
@ -109,6 +113,7 @@ func (c *Client) ListUserReposIter() iter.Seq2[*gitea.Repository, error] {
}
// GetRepo returns a single repository by owner and name.
// Usage: GetRepo(...)
func (c *Client) GetRepo(owner, name string) (*gitea.Repository, error) {
repo, _, err := c.api.GetRepo(owner, name)
if err != nil {
@ -121,6 +126,7 @@ func (c *Client) GetRepo(owner, name string) (*gitea.Repository, error) {
// CreateMirror creates a mirror repository on Gitea from a GitHub clone URL.
// This uses the Gitea migration API to set up a pull mirror.
// If authToken is provided, it is used to authenticate against the source (e.g. for private GitHub repos).
// Usage: CreateMirror(...)
func (c *Client) CreateMirror(owner, name, cloneURL, authToken string) (*gitea.Repository, error) {
opts := gitea.MigrateRepoOption{
RepoName: name,
@ -144,6 +150,7 @@ func (c *Client) CreateMirror(owner, name, cloneURL, authToken string) (*gitea.R
}
// DeleteRepo deletes a repository from Gitea.
// Usage: DeleteRepo(...)
func (c *Client) DeleteRepo(owner, name string) error {
_, err := c.api.DeleteRepo(owner, name)
if err != nil {
@ -154,6 +161,7 @@ func (c *Client) DeleteRepo(owner, name string) error {
}
// CreateOrgRepo creates a new empty repository under an organisation.
// Usage: CreateOrgRepo(...)
func (c *Client) CreateOrgRepo(org string, opts gitea.CreateRepoOption) (*gitea.Repository, error) {
repo, _, err := c.api.CreateOrgRepo(org, opts)
if err != nil {

View file

@ -11,6 +11,7 @@ import (
const Separator = '/'
// Abs mirrors filepath.Abs for the paths used in this repo.
// Usage: Abs(...)
func Abs(p string) (string, error) {
if path.IsAbs(p) {
return path.Clean(p), nil
@ -23,26 +24,31 @@ func Abs(p string) (string, error) {
}
// Base mirrors filepath.Base.
// Usage: Base(...)
func Base(p string) string {
return path.Base(p)
}
// Clean mirrors filepath.Clean.
// Usage: Clean(...)
func Clean(p string) string {
return path.Clean(p)
}
// Dir mirrors filepath.Dir.
// Usage: Dir(...)
func Dir(p string) string {
return path.Dir(p)
}
// Ext mirrors filepath.Ext.
// Usage: Ext(...)
func Ext(p string) string {
return path.Ext(p)
}
// Join mirrors filepath.Join.
// Usage: Join(...)
func Join(elem ...string) string {
return path.Join(elem...)
}

View file

@ -10,26 +10,31 @@ import (
)
// Sprint mirrors fmt.Sprint using Core primitives.
// Usage: Sprint(...)
func Sprint(args ...any) string {
return core.Sprint(args...)
}
// Sprintf mirrors fmt.Sprintf using Core primitives.
// Usage: Sprintf(...)
func Sprintf(format string, args ...any) string {
return core.Sprintf(format, args...)
}
// Fprintf mirrors fmt.Fprintf using Core primitives.
// Usage: Fprintf(...)
func Fprintf(w io.Writer, format string, args ...any) (int, error) {
return io.WriteString(w, Sprintf(format, args...))
}
// Printf mirrors fmt.Printf.
// Usage: Printf(...)
func Printf(format string, args ...any) (int, error) {
return Fprintf(stdio.Stdout, format, args...)
}
// Println mirrors fmt.Println.
// Usage: Println(...)
func Println(args ...any) (int, error) {
return io.WriteString(stdio.Stdout, Sprint(args...)+"\n")
}

View file

@ -9,26 +9,31 @@ import (
)
// Marshal mirrors encoding/json.Marshal.
// Usage: Marshal(...)
func Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
// MarshalIndent mirrors encoding/json.MarshalIndent.
// Usage: MarshalIndent(...)
func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
// NewDecoder mirrors encoding/json.NewDecoder.
// Usage: NewDecoder(...)
func NewDecoder(r io.Reader) *json.Decoder {
return json.NewDecoder(r)
}
// NewEncoder mirrors encoding/json.NewEncoder.
// Usage: NewEncoder(...)
func NewEncoder(w io.Writer) *json.Encoder {
return json.NewEncoder(w)
}
// Unmarshal mirrors encoding/json.Unmarshal.
// Usage: Unmarshal(...)
func Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}

View file

@ -32,32 +32,38 @@ var Stdout = stdio.Stdout
var Stderr = stdio.Stderr
// Getenv mirrors os.Getenv.
// Usage: Getenv(...)
func Getenv(key string) string {
value, _ := syscall.Getenv(key)
return value
}
// Getwd mirrors os.Getwd.
// Usage: Getwd(...)
func Getwd() (string, error) {
return syscall.Getwd()
}
// IsNotExist mirrors os.IsNotExist.
// Usage: IsNotExist(...)
func IsNotExist(err error) bool {
return core.Is(err, fs.ErrNotExist)
}
// MkdirAll mirrors os.MkdirAll.
// Usage: MkdirAll(...)
func MkdirAll(path string, _ fs.FileMode) error {
return coreio.Local.EnsureDir(path)
}
// Open mirrors os.Open.
// Usage: Open(...)
func Open(path string) (fs.File, error) {
return coreio.Local.Open(path)
}
// OpenFile mirrors the append/create/write mode used in this repo.
// Usage: OpenFile(...)
func OpenFile(path string, flag int, _ fs.FileMode) (io.WriteCloser, error) {
if flag&O_APPEND != 0 {
return coreio.Local.Append(path)
@ -66,22 +72,26 @@ func OpenFile(path string, flag int, _ fs.FileMode) (io.WriteCloser, error) {
}
// ReadDir mirrors os.ReadDir.
// Usage: ReadDir(...)
func ReadDir(path string) ([]fs.DirEntry, error) {
return coreio.Local.List(path)
}
// ReadFile mirrors os.ReadFile.
// Usage: ReadFile(...)
func ReadFile(path string) ([]byte, error) {
content, err := coreio.Local.Read(path)
return []byte(content), err
}
// Stat mirrors os.Stat.
// Usage: Stat(...)
func Stat(path string) (fs.FileInfo, error) {
return coreio.Local.Stat(path)
}
// UserHomeDir mirrors os.UserHomeDir.
// Usage: UserHomeDir(...)
func UserHomeDir() (string, error) {
if home := Getenv("HOME"); home != "" {
return home, nil
@ -94,6 +104,7 @@ func UserHomeDir() (string, error) {
}
// WriteFile mirrors os.WriteFile.
// Usage: WriteFile(...)
func WriteFile(path string, data []byte, perm fs.FileMode) error {
return coreio.Local.WriteMode(path, string(data), perm)
}

View file

@ -11,6 +11,8 @@ type fdReader struct {
fd int
}
// Read implements io.Reader for stdin without importing os.
// Usage: Read(...)
func (r fdReader) Read(p []byte) (int, error) {
n, err := syscall.Read(r.fd, p)
if n == 0 && err == nil {
@ -23,6 +25,8 @@ type fdWriter struct {
fd int
}
// Write implements io.Writer for stdout and stderr without importing os.
// Usage: Write(...)
func (w fdWriter) Write(p []byte) (int, error) {
return syscall.Write(w.fd, p)
}

View file

@ -14,21 +14,25 @@ import (
type Builder = bytes.Buffer
// Contains mirrors strings.Contains.
// Usage: Contains(...)
func Contains(s, substr string) bool {
return core.Contains(s, substr)
}
// ContainsAny mirrors strings.ContainsAny.
// Usage: ContainsAny(...)
func ContainsAny(s, chars string) bool {
return bytes.IndexAny([]byte(s), chars) >= 0
}
// EqualFold mirrors strings.EqualFold.
// Usage: EqualFold(...)
func EqualFold(s, t string) bool {
return bytes.EqualFold([]byte(s), []byte(t))
}
// Fields mirrors strings.Fields.
// Usage: Fields(...)
func Fields(s string) []string {
scanner := bufio.NewScanner(NewReader(s))
scanner.Split(bufio.ScanWords)
@ -40,31 +44,37 @@ func Fields(s string) []string {
}
// HasPrefix mirrors strings.HasPrefix.
// Usage: HasPrefix(...)
func HasPrefix(s, prefix string) bool {
return core.HasPrefix(s, prefix)
}
// HasSuffix mirrors strings.HasSuffix.
// Usage: HasSuffix(...)
func HasSuffix(s, suffix string) bool {
return core.HasSuffix(s, suffix)
}
// Join mirrors strings.Join.
// Usage: Join(...)
func Join(elems []string, sep string) string {
return core.Join(sep, elems...)
}
// LastIndex mirrors strings.LastIndex.
// Usage: LastIndex(...)
func LastIndex(s, substr string) int {
return bytes.LastIndex([]byte(s), []byte(substr))
}
// NewReader mirrors strings.NewReader.
// Usage: NewReader(...)
func NewReader(s string) *bytes.Reader {
return bytes.NewReader([]byte(s))
}
// Repeat mirrors strings.Repeat.
// Usage: Repeat(...)
func Repeat(s string, count int) string {
if count <= 0 {
return ""
@ -73,26 +83,31 @@ func Repeat(s string, count int) string {
}
// ReplaceAll mirrors strings.ReplaceAll.
// Usage: ReplaceAll(...)
func ReplaceAll(s, old, new string) string {
return core.Replace(s, old, new)
}
// Replace mirrors strings.Replace for replace-all call sites.
// Usage: Replace(...)
func Replace(s, old, new string, _ int) string {
return ReplaceAll(s, old, new)
}
// Split mirrors strings.Split.
// Usage: Split(...)
func Split(s, sep string) []string {
return core.Split(s, sep)
}
// SplitN mirrors strings.SplitN.
// Usage: SplitN(...)
func SplitN(s, sep string, n int) []string {
return core.SplitN(s, sep, n)
}
// SplitSeq mirrors strings.SplitSeq.
// Usage: SplitSeq(...)
func SplitSeq(s, sep string) iter.Seq[string] {
parts := Split(s, sep)
return func(yield func(string) bool) {
@ -105,26 +120,31 @@ func SplitSeq(s, sep string) iter.Seq[string] {
}
// ToLower mirrors strings.ToLower.
// Usage: ToLower(...)
func ToLower(s string) string {
return core.Lower(s)
}
// ToUpper mirrors strings.ToUpper.
// Usage: ToUpper(...)
func ToUpper(s string) string {
return core.Upper(s)
}
// TrimPrefix mirrors strings.TrimPrefix.
// Usage: TrimPrefix(...)
func TrimPrefix(s, prefix string) string {
return core.TrimPrefix(s, prefix)
}
// TrimSpace mirrors strings.TrimSpace.
// Usage: TrimSpace(...)
func TrimSpace(s string) string {
return core.Trim(s)
}
// TrimSuffix mirrors strings.TrimSuffix.
// Usage: TrimSuffix(...)
func TrimSuffix(s, suffix string) string {
return core.TrimSuffix(s, suffix)
}

View file

@ -24,6 +24,7 @@ type ForgejoSource struct {
}
// New creates a ForgejoSource using the given forge client.
// Usage: New(...)
func New(cfg Config, client *forge.Client) *ForgejoSource {
return &ForgejoSource{
repos: cfg.Repos,
@ -32,12 +33,14 @@ func New(cfg Config, client *forge.Client) *ForgejoSource {
}
// Name returns the source identifier.
// Usage: Name(...)
func (s *ForgejoSource) Name() string {
return "forgejo"
}
// Poll fetches epics and their linked PRs from all configured repositories,
// returning a PipelineSignal for each unchecked child that has a linked PR.
// Usage: Poll(...)
func (s *ForgejoSource) Poll(ctx context.Context) ([]*jobrunner.PipelineSignal, error) {
var signals []*jobrunner.PipelineSignal
@ -61,6 +64,7 @@ func (s *ForgejoSource) Poll(ctx context.Context) ([]*jobrunner.PipelineSignal,
}
// Report posts the action result as a comment on the epic issue.
// Usage: Report(...)
func (s *ForgejoSource) Report(ctx context.Context, result *jobrunner.ActionResult) error {
if result == nil {
return nil

View file

@ -23,6 +23,7 @@ type CompletionHandler struct {
}
// NewCompletionHandler creates a handler for agent completion events.
// Usage: NewCompletionHandler(...)
func NewCompletionHandler(client *forge.Client) *CompletionHandler {
return &CompletionHandler{
forge: client,

View file

@ -62,6 +62,7 @@ type DispatchHandler struct {
}
// NewDispatchHandler creates a handler that dispatches tickets to agent machines.
// Usage: NewDispatchHandler(...)
func NewDispatchHandler(client *forge.Client, forgeURL, token string, spinner *agentci.Spinner) *DispatchHandler {
return &DispatchHandler{
forge: client,
@ -72,12 +73,14 @@ func NewDispatchHandler(client *forge.Client, forgeURL, token string, spinner *a
}
// Name returns the handler identifier.
// Usage: Name(...)
func (h *DispatchHandler) Name() string {
return "dispatch"
}
// Match returns true for signals where a child issue needs coding (no PR yet)
// and the assignee is a known agent (by config key or Forgejo username).
// Usage: Match(...)
func (h *DispatchHandler) Match(signal *jobrunner.PipelineSignal) bool {
if !signal.NeedsCoding {
return false
@ -87,6 +90,7 @@ func (h *DispatchHandler) Match(signal *jobrunner.PipelineSignal) bool {
}
// Execute creates a ticket JSON and transfers it securely to the agent's queue directory.
// Usage: Execute(...)
func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.PipelineSignal) (*jobrunner.ActionResult, error) {
start := time.Now()

View file

@ -17,6 +17,7 @@ type EnableAutoMergeHandler struct {
}
// NewEnableAutoMergeHandler creates a handler that merges ready PRs.
// Usage: NewEnableAutoMergeHandler(...)
func NewEnableAutoMergeHandler(f *forge.Client) *EnableAutoMergeHandler {
return &EnableAutoMergeHandler{forge: f}
}

View file

@ -17,6 +17,7 @@ type PublishDraftHandler struct {
}
// NewPublishDraftHandler creates a handler that publishes draft PRs.
// Usage: NewPublishDraftHandler(...)
func NewPublishDraftHandler(f *forge.Client) *PublishDraftHandler {
return &PublishDraftHandler{forge: f}
}

View file

@ -22,6 +22,7 @@ type DismissReviewsHandler struct {
}
// NewDismissReviewsHandler creates a handler that dismisses stale reviews.
// Usage: NewDismissReviewsHandler(...)
func NewDismissReviewsHandler(f *forge.Client) *DismissReviewsHandler {
return &DismissReviewsHandler{forge: f}
}

View file

@ -18,6 +18,7 @@ type SendFixCommandHandler struct {
}
// NewSendFixCommandHandler creates a handler that posts fix commands.
// Usage: NewSendFixCommandHandler(...)
func NewSendFixCommandHandler(f *forge.Client) *SendFixCommandHandler {
return &SendFixCommandHandler{forge: f}
}

View file

@ -22,6 +22,7 @@ type TickParentHandler struct {
}
// NewTickParentHandler creates a handler that ticks parent epic checkboxes.
// Usage: NewTickParentHandler(...)
func NewTickParentHandler(f *forge.Client) *TickParentHandler {
return &TickParentHandler{forge: f}
}

View file

@ -54,6 +54,7 @@ type Journal struct {
}
// NewJournal creates a new Journal rooted at baseDir.
// Usage: NewJournal(...)
func NewJournal(baseDir string) (*Journal, error) {
if baseDir == "" {
return nil, coreerr.E("jobrunner.NewJournal", "base directory is required", nil)
@ -92,6 +93,7 @@ func sanitizePathComponent(name string) (string, error) {
}
// Append writes a journal entry for the given signal and result.
// Usage: Append(...)
func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error {
if signal == nil {
return coreerr.E("jobrunner.Journal.Append", "signal is required", nil)

View file

@ -31,6 +31,7 @@ type Poller struct {
}
// NewPoller creates a Poller from the given config.
// Usage: NewPoller(...)
func NewPoller(cfg PollerConfig) *Poller {
interval := cfg.PollInterval
if interval <= 0 {
@ -47,6 +48,7 @@ func NewPoller(cfg PollerConfig) *Poller {
}
// Cycle returns the number of completed poll-dispatch cycles.
// Usage: Cycle(...)
func (p *Poller) Cycle() int {
p.mu.RLock()
defer p.mu.RUnlock()
@ -54,6 +56,7 @@ func (p *Poller) Cycle() int {
}
// DryRun returns whether dry-run mode is enabled.
// Usage: DryRun(...)
func (p *Poller) DryRun() bool {
p.mu.RLock()
defer p.mu.RUnlock()
@ -61,6 +64,7 @@ func (p *Poller) DryRun() bool {
}
// SetDryRun enables or disables dry-run mode.
// Usage: SetDryRun(...)
func (p *Poller) SetDryRun(v bool) {
p.mu.Lock()
p.dryRun = v
@ -68,6 +72,7 @@ func (p *Poller) SetDryRun(v bool) {
}
// AddSource appends a source to the poller.
// Usage: AddSource(...)
func (p *Poller) AddSource(s JobSource) {
p.mu.Lock()
p.sources = append(p.sources, s)
@ -75,6 +80,7 @@ func (p *Poller) AddSource(s JobSource) {
}
// AddHandler appends a handler to the poller.
// Usage: AddHandler(...)
func (p *Poller) AddHandler(h JobHandler) {
p.mu.Lock()
p.handlers = append(p.handlers, h)
@ -84,6 +90,7 @@ func (p *Poller) AddHandler(h JobHandler) {
// Run starts a blocking poll-dispatch loop. It runs one cycle immediately,
// then repeats on each tick of the configured interval until the context
// is cancelled.
// Usage: Run(...)
func (p *Poller) Run(ctx context.Context) error {
if err := p.RunOnce(ctx); err != nil {
return err
@ -106,6 +113,7 @@ func (p *Poller) Run(ctx context.Context) error {
// RunOnce performs a single poll-dispatch cycle: iterate sources, poll each,
// find the first matching handler for each signal, and execute it.
// Usage: RunOnce(...)
func (p *Poller) RunOnce(ctx context.Context) error {
p.mu.Lock()
p.cycle++

View file

@ -35,11 +35,13 @@ type PipelineSignal struct {
}
// RepoFullName returns "owner/repo".
// Usage: RepoFullName(...)
func (s *PipelineSignal) RepoFullName() string {
return s.RepoOwner + "/" + s.RepoName
}
// HasUnresolvedThreads returns true if there are unresolved review threads.
// Usage: HasUnresolvedThreads(...)
func (s *PipelineSignal) HasUnresolvedThreads() bool {
return s.ThreadsTotal > s.ThreadsResolved
}

View file

@ -35,6 +35,7 @@ type CompileOptions struct {
// Compile produces a CompiledManifest from a source manifest and build
// options. If opts.SignKey is provided the manifest is signed first.
// Usage: Compile(...)
func Compile(m *Manifest, opts CompileOptions) (*CompiledManifest, error) {
if m == nil {
return nil, coreerr.E("manifest.Compile", "nil manifest", nil)
@ -63,11 +64,13 @@ func Compile(m *Manifest, opts CompileOptions) (*CompiledManifest, error) {
}
// MarshalJSON serialises a CompiledManifest to JSON bytes.
// Usage: MarshalJSON(...)
func MarshalJSON(cm *CompiledManifest) ([]byte, error) {
return json.MarshalIndent(cm, "", " ")
}
// ParseCompiled decodes a core.json into a CompiledManifest.
// Usage: ParseCompiled(...)
func ParseCompiled(data []byte) (*CompiledManifest, error) {
var cm CompiledManifest
if err := json.Unmarshal(data, &cm); err != nil {
@ -80,6 +83,7 @@ const compiledPath = "core.json"
// WriteCompiled writes a CompiledManifest as core.json to the given root
// directory. The file lives at the distribution root, not inside .core/.
// Usage: WriteCompiled(...)
func WriteCompiled(medium io.Medium, root string, cm *CompiledManifest) error {
data, err := MarshalJSON(cm)
if err != nil {
@ -90,6 +94,7 @@ func WriteCompiled(medium io.Medium, root string, cm *CompiledManifest) error {
}
// LoadCompiled reads and parses a core.json from the given root directory.
// Usage: LoadCompiled(...)
func LoadCompiled(medium io.Medium, root string) (*CompiledManifest, error) {
path := filepath.Join(root, compiledPath)
data, err := medium.Read(path)

View file

@ -14,11 +14,13 @@ import (
const manifestPath = ".core/manifest.yaml"
// MarshalYAML serializes a manifest to YAML bytes.
// Usage: MarshalYAML(...)
func MarshalYAML(m *Manifest) ([]byte, error) {
return yaml.Marshal(m)
}
// Load reads and parses a .core/manifest.yaml from the given root directory.
// Usage: Load(...)
func Load(medium io.Medium, root string) (*Manifest, error) {
path := filepath.Join(root, manifestPath)
data, err := medium.Read(path)
@ -29,6 +31,7 @@ func Load(medium io.Medium, root string) (*Manifest, error) {
}
// LoadVerified reads, parses, and verifies the ed25519 signature.
// Usage: LoadVerified(...)
func LoadVerified(medium io.Medium, root string, pub ed25519.PublicKey) (*Manifest, error) {
m, err := Load(medium, root)
if err != nil {

View file

@ -67,6 +67,8 @@ type DaemonSpec struct {
// Parse decodes YAML bytes into a Manifest.
//
// m, err := manifest.Parse(yamlBytes)
//
// Usage: Parse(...)
func Parse(data []byte) (*Manifest, error) {
var m Manifest
if err := yaml.Unmarshal(data, &m); err != nil {
@ -76,6 +78,7 @@ func Parse(data []byte) (*Manifest, error) {
}
// SlotNames returns a deduplicated list of component names from slots.
// Usage: SlotNames(...)
func (m *Manifest) SlotNames() []string {
seen := make(map[string]bool)
var names []string
@ -92,6 +95,7 @@ func (m *Manifest) SlotNames() []string {
// A daemon is the default if it has Default:true, or if it is the only daemon
// in the map. If multiple daemons have Default:true, returns false (ambiguous).
// Returns empty values and false if no default can be determined.
// Usage: DefaultDaemon(...)
func (m *Manifest) DefaultDaemon() (string, DaemonSpec, bool) {
if len(m.Daemons) == 0 {
return "", DaemonSpec{}, false

View file

@ -18,6 +18,7 @@ func signable(m *Manifest) ([]byte, error) {
}
// Sign computes the ed25519 signature and stores it in m.Sign (base64).
// Usage: Sign(...)
func Sign(m *Manifest, priv ed25519.PrivateKey) error {
msg, err := signable(m)
if err != nil {
@ -29,6 +30,7 @@ func Sign(m *Manifest, priv ed25519.PrivateKey) error {
}
// Verify checks the ed25519 signature in m.Sign against the public key.
// Usage: Verify(...)
func Verify(m *Manifest, pub ed25519.PublicKey) (bool, error) {
if m.Sign == "" {
return false, coreerr.E("manifest.Verify", "no signature present", nil)

View file

@ -33,6 +33,7 @@ type Builder struct {
// BuildFromDirs scans each directory for subdirectories containing either
// core.json (preferred) or .core/manifest.yaml. Each valid manifest is
// added to the resulting Index as a Module.
// Usage: BuildFromDirs(...)
func (b *Builder) BuildFromDirs(dirs ...string) (*Index, error) {
var modules []Module
seen := make(map[string]bool)
@ -86,6 +87,7 @@ func (b *Builder) BuildFromDirs(dirs ...string) (*Index, error) {
// BuildFromManifests constructs an Index from pre-loaded manifests.
// This is useful when manifests have already been collected (e.g. from
// a Forge API crawl).
// Usage: BuildFromManifests(...)
func BuildFromManifests(manifests []*manifest.Manifest) *Index {
var modules []Module
seen := make(map[string]bool)
@ -116,6 +118,7 @@ func BuildFromManifests(manifests []*manifest.Manifest) *Index {
}
// WriteIndex serialises an Index to JSON and writes it to the given path.
// Usage: WriteIndex(...)
func WriteIndex(path string, idx *Index) error {
if err := coreio.Local.EnsureDir(filepath.Dir(path)); err != nil {
return coreerr.E("marketplace.WriteIndex", "mkdir failed", err)

View file

@ -26,6 +26,7 @@ type DiscoveredProvider struct {
// Each subdirectory is checked for a .core/manifest.yaml file. Directories
// without a valid manifest are skipped with a log warning.
// Only manifests with provider fields (namespace + binary) are returned.
// Usage: DiscoverProviders(...)
func DiscoverProviders(dir string) ([]DiscoveredProvider, error) {
entries, err := os.ReadDir(dir)
if err != nil {
@ -86,6 +87,7 @@ type ProviderRegistryFile struct {
// LoadProviderRegistry reads a registry.yaml file from the given path.
// Returns an empty registry if the file does not exist.
// Usage: LoadProviderRegistry(...)
func LoadProviderRegistry(path string) (*ProviderRegistryFile, error) {
raw, err := coreio.Local.Read(path)
if err != nil {
@ -111,6 +113,7 @@ func LoadProviderRegistry(path string) (*ProviderRegistryFile, error) {
}
// SaveProviderRegistry writes the registry to the given path.
// Usage: SaveProviderRegistry(...)
func SaveProviderRegistry(path string, reg *ProviderRegistryFile) error {
if err := coreio.Local.EnsureDir(filepath.Dir(path)); err != nil {
return coreerr.E("marketplace.SaveProviderRegistry", "ensure directory", err)
@ -125,6 +128,7 @@ func SaveProviderRegistry(path string, reg *ProviderRegistryFile) error {
}
// Add adds or updates a provider entry in the registry.
// Usage: Add(...)
func (r *ProviderRegistryFile) Add(code string, entry ProviderRegistryEntry) {
if r.Providers == nil {
r.Providers = make(map[string]ProviderRegistryEntry)
@ -133,17 +137,20 @@ func (r *ProviderRegistryFile) Add(code string, entry ProviderRegistryEntry) {
}
// Remove removes a provider entry from the registry.
// Usage: Remove(...)
func (r *ProviderRegistryFile) Remove(code string) {
delete(r.Providers, code)
}
// Get returns a provider entry and true if found, or zero value and false.
// Usage: Get(...)
func (r *ProviderRegistryFile) Get(code string) (ProviderRegistryEntry, bool) {
entry, ok := r.Providers[code]
return entry, ok
}
// List returns all provider codes in the registry.
// Usage: List(...)
func (r *ProviderRegistryFile) List() []string {
codes := make([]string, 0, len(r.Providers))
for code := range r.Providers {
@ -153,6 +160,7 @@ func (r *ProviderRegistryFile) List() []string {
}
// AutoStartProviders returns codes of providers with auto_start enabled.
// Usage: AutoStartProviders(...)
func (r *ProviderRegistryFile) AutoStartProviders() []string {
var codes []string
for code, entry := range r.Providers {

View file

@ -28,6 +28,7 @@ type Installer struct {
}
// NewInstaller creates a new module installer.
// Usage: NewInstaller(...)
func NewInstaller(m io.Medium, modulesDir string, st *store.Store) *Installer {
return &Installer{
medium: m,
@ -49,6 +50,7 @@ type InstalledModule struct {
}
// Install clones a module repo, verifies its manifest signature, and registers it.
// Usage: Install(...)
func (i *Installer) Install(ctx context.Context, mod Module) error {
safeCode, dest, err := i.resolveModulePath(mod.Code)
if err != nil {
@ -111,6 +113,7 @@ func (i *Installer) Install(ctx context.Context, mod Module) error {
}
// Remove uninstalls a module by deleting its files and store entry.
// Usage: Remove(...)
func (i *Installer) Remove(code string) error {
safeCode, dest, err := i.resolveModulePath(code)
if err != nil {
@ -129,6 +132,7 @@ func (i *Installer) Remove(code string) error {
}
// Update pulls latest changes and re-verifies the manifest.
// Usage: Update(...)
func (i *Installer) Update(ctx context.Context, code string) error {
safeCode, dest, err := i.resolveModulePath(code)
if err != nil {
@ -175,6 +179,7 @@ func (i *Installer) Update(ctx context.Context, code string) error {
}
// Installed returns all installed module metadata.
// Usage: Installed(...)
func (i *Installer) Installed() ([]InstalledModule, error) {
all, err := i.store.GetAll(storeGroup)
if err != nil {

View file

@ -26,6 +26,7 @@ type Index struct {
}
// ParseIndex decodes a marketplace index.json.
// Usage: ParseIndex(...)
func ParseIndex(data []byte) (*Index, error) {
var idx Index
if err := json.Unmarshal(data, &idx); err != nil {
@ -35,6 +36,7 @@ func ParseIndex(data []byte) (*Index, error) {
}
// Search returns modules matching the query in code, name, or category.
// Usage: Search(...)
func (idx *Index) Search(query string) []Module {
q := strings.ToLower(query)
var results []Module
@ -49,6 +51,7 @@ func (idx *Index) Search(query string) []Module {
}
// ByCategory returns all modules in the given category.
// Usage: ByCategory(...)
func (idx *Index) ByCategory(category string) []Module {
var results []Module
for _, m := range idx.Modules {
@ -60,6 +63,7 @@ func (idx *Index) ByCategory(category string) []Module {
}
// Find returns the module with the given code, or false if not found.
// Usage: Find(...)
func (idx *Index) Find(code string) (Module, bool) {
for _, m := range idx.Modules {
if m.Code == code {

View file

@ -45,6 +45,7 @@ var (
// NewProvider creates an SCM provider backed by the given marketplace index,
// installer, and registry. The WS hub is used to emit real-time events.
// Pass nil for any dependency that is not available.
// Usage: NewProvider(...)
func NewProvider(idx *marketplace.Index, inst *marketplace.Installer, reg *repos.Registry, hub *ws.Hub) *ScmProvider {
return &ScmProvider{
index: idx,
@ -56,12 +57,15 @@ func NewProvider(idx *marketplace.Index, inst *marketplace.Installer, reg *repos
}
// Name implements api.RouteGroup.
// Usage: Name(...)
func (p *ScmProvider) Name() string { return "scm" }
// BasePath implements api.RouteGroup.
// Usage: BasePath(...)
func (p *ScmProvider) BasePath() string { return "/api/v1/scm" }
// Element implements provider.Renderable.
// Usage: Element(...)
func (p *ScmProvider) Element() provider.ElementSpec {
return provider.ElementSpec{
Tag: "core-scm-panel",
@ -70,6 +74,7 @@ func (p *ScmProvider) Element() provider.ElementSpec {
}
// Channels implements provider.Streamable.
// Usage: Channels(...)
func (p *ScmProvider) Channels() []string {
return []string{
"scm.marketplace.refreshed",
@ -81,6 +86,7 @@ func (p *ScmProvider) Channels() []string {
}
// RegisterRoutes implements api.RouteGroup.
// Usage: RegisterRoutes(...)
func (p *ScmProvider) RegisterRoutes(rg *gin.RouterGroup) {
// Marketplace
rg.GET("/marketplace", p.listMarketplace)
@ -103,6 +109,7 @@ func (p *ScmProvider) RegisterRoutes(rg *gin.RouterGroup) {
}
// Describe implements api.DescribableGroup.
// Usage: Describe(...)
func (p *ScmProvider) Describe() []api.RouteDescription {
return []api.RouteDescription{
{

View file

@ -23,6 +23,7 @@ type Installer struct {
}
// NewInstaller creates a new plugin installer.
// Usage: NewInstaller(...)
func NewInstaller(m io.Medium, registry *Registry) *Installer {
return &Installer{
medium: m,
@ -183,6 +184,8 @@ func (i *Installer) cloneRepo(ctx context.Context, org, repo, version, dest stri
// Accepted formats:
// - "org/repo" -> org="org", repo="repo", version=""
// - "org/repo@v1.0" -> org="org", repo="repo", version="v1.0"
//
// Usage: ParseSource(...)
func ParseSource(source string) (org, repo, version string, err error) {
source, err = url.PathUnescape(source)
if err != nil {

View file

@ -16,6 +16,7 @@ type Loader struct {
}
// NewLoader creates a new plugin loader.
// Usage: NewLoader(...)
func NewLoader(m io.Medium, baseDir string) *Loader {
return &Loader{
medium: m,

View file

@ -22,6 +22,7 @@ type Manifest struct {
}
// LoadManifest reads and parses a plugin.json file from the given path.
// Usage: LoadManifest(...)
func LoadManifest(m io.Medium, path string) (*Manifest, error) {
content, err := m.Read(path)
if err != nil {

View file

@ -23,6 +23,7 @@ type Registry struct {
}
// NewRegistry creates a new plugin registry.
// Usage: NewRegistry(...)
func NewRegistry(m io.Medium, basePath string) *Registry {
return &Registry{
medium: m,

View file

@ -37,6 +37,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.
// Usage: LoadGitState(...)
func LoadGitState(m io.Medium, root string) (*GitState, error) {
path := filepath.Join(root, ".core", "git.yaml")
@ -65,6 +66,7 @@ func LoadGitState(m io.Medium, root string) (*GitState, error) {
}
// SaveGitState writes .core/git.yaml to the given workspace root directory.
// Usage: SaveGitState(...)
func SaveGitState(m io.Medium, root string, gs *GitState) error {
coreDir := filepath.Join(root, ".core")
if err := m.EnsureDir(coreDir); err != nil {
@ -85,6 +87,7 @@ func SaveGitState(m io.Medium, root string, gs *GitState) error {
}
// NewGitState returns a new empty GitState with version 1.
// Usage: NewGitState(...)
func NewGitState() *GitState {
return &GitState{
Version: 1,
@ -94,16 +97,19 @@ func NewGitState() *GitState {
}
// Touch records a pull timestamp for the named repo.
// Usage: TouchPull(...)
func (gs *GitState) TouchPull(name string) {
gs.ensureRepo(name).LastPull = time.Now()
}
// TouchPush records a push timestamp for the named repo.
// Usage: TouchPush(...)
func (gs *GitState) TouchPush(name string) {
gs.ensureRepo(name).LastPush = time.Now()
}
// UpdateRepo records the current git status for a repo.
// Usage: UpdateRepo(...)
func (gs *GitState) UpdateRepo(name, branch, remote string, ahead, behind int) {
r := gs.ensureRepo(name)
r.Branch = branch
@ -113,6 +119,7 @@ func (gs *GitState) UpdateRepo(name, branch, remote string, ahead, behind int) {
}
// Heartbeat records an agent's presence and active packages.
// Usage: Heartbeat(...)
func (gs *GitState) Heartbeat(agentName string, active []string) {
if gs.Agents == nil {
gs.Agents = make(map[string]*AgentState)
@ -124,6 +131,7 @@ func (gs *GitState) Heartbeat(agentName string, active []string) {
}
// StaleAgents returns agent names whose last heartbeat is older than the given duration.
// Usage: StaleAgents(...)
func (gs *GitState) StaleAgents(staleAfter time.Duration) []string {
cutoff := time.Now().Add(-staleAfter)
var stale []string
@ -137,6 +145,7 @@ func (gs *GitState) StaleAgents(staleAfter time.Duration) []string {
// ActiveAgentsFor returns agent names that have the given repo in their active list
// and are not stale.
// Usage: ActiveAgentsFor(...)
func (gs *GitState) ActiveAgentsFor(repoName string, staleAfter time.Duration) []string {
cutoff := time.Now().Add(-staleAfter)
var agents []string
@ -155,6 +164,7 @@ func (gs *GitState) ActiveAgentsFor(repoName string, staleAfter time.Duration) [
}
// NeedsPull returns true if the repo has never been pulled or was pulled before the given duration.
// Usage: NeedsPull(...)
func (gs *GitState) NeedsPull(name string, maxAge time.Duration) bool {
r, ok := gs.Repos[name]
if !ok {

View file

@ -47,6 +47,7 @@ type KBSearch struct {
}
// DefaultKBConfig returns sensible defaults for knowledge base config.
// Usage: DefaultKBConfig(...)
func DefaultKBConfig() *KBConfig {
return &KBConfig{
Version: 1,
@ -68,6 +69,7 @@ func DefaultKBConfig() *KBConfig {
// LoadKBConfig reads .core/kb.yaml from the given workspace root directory.
// Returns defaults if the file does not exist.
// Usage: LoadKBConfig(...)
func LoadKBConfig(m io.Medium, root string) (*KBConfig, error) {
path := filepath.Join(root, ".core", "kb.yaml")
@ -89,6 +91,7 @@ func LoadKBConfig(m io.Medium, root string) (*KBConfig, error) {
}
// SaveKBConfig writes .core/kb.yaml to the given workspace root directory.
// Usage: SaveKBConfig(...)
func SaveKBConfig(m io.Medium, root string, kb *KBConfig) error {
coreDir := filepath.Join(root, ".core")
if err := m.EnsureDir(coreDir); err != nil {
@ -109,11 +112,13 @@ func SaveKBConfig(m io.Medium, root string, kb *KBConfig) error {
}
// WikiRepoURL returns the full clone URL for a repo's wiki.
// Usage: WikiRepoURL(...)
func (kb *KBConfig) WikiRepoURL(repoName string) string {
return fmt.Sprintf("%s/%s.wiki.git", kb.Wiki.Remote, repoName)
}
// WikiLocalPath returns the local path for a repo's wiki clone.
// Usage: WikiLocalPath(...)
func (kb *KBConfig) WikiLocalPath(root, repoName string) string {
return filepath.Join(root, ".core", kb.Wiki.Dir, repoName)
}

View file

@ -71,6 +71,8 @@ type Repo struct {
// The path should be a valid path for the provided medium.
//
// reg, err := repos.LoadRegistry(io.Local, ".core/repos.yaml")
//
// Usage: LoadRegistry(...)
func LoadRegistry(m io.Medium, path string) (*Registry, error) {
content, err := m.Read(path)
if err != nil {
@ -112,6 +114,8 @@ func LoadRegistry(m io.Medium, path string) (*Registry, error) {
// This function is primarily intended for use with io.Local or other local-like filesystems.
//
// path, err := repos.FindRegistry(io.Local)
//
// Usage: FindRegistry(...)
func FindRegistry(m io.Medium) (string, error) {
// Check current directory and parents
dir, err := os.Getwd()
@ -164,6 +168,8 @@ func FindRegistry(m io.Medium) (string, error) {
// The dir should be a valid path for the provided medium.
//
// reg, err := repos.ScanDirectory(io.Local, "/home/user/Code/core")
//
// Usage: ScanDirectory(...)
func ScanDirectory(m io.Medium, dir string) (*Registry, error) {
entries, err := m.List(dir)
if err != nil {
@ -255,6 +261,8 @@ func detectOrg(m io.Medium, repoPath string) string {
// List returns all repos in the registry.
//
// repos := reg.List()
//
// Usage: List(...)
func (r *Registry) List() []*Repo {
repos := make([]*Repo, 0, len(r.Repos))
for _, repo := range r.Repos {
@ -267,6 +275,8 @@ func (r *Registry) List() []*Repo {
// Get returns a repo by name.
//
// repo, ok := reg.Get("go-io")
//
// Usage: Get(...)
func (r *Registry) Get(name string) (*Repo, bool) {
repo, ok := r.Repos[name]
return repo, ok
@ -275,6 +285,8 @@ func (r *Registry) Get(name string) (*Repo, bool) {
// ByType returns repos filtered by type.
//
// goRepos := reg.ByType("go")
//
// Usage: ByType(...)
func (r *Registry) ByType(t string) []*Repo {
var repos []*Repo
for _, repo := range r.Repos {
@ -289,6 +301,8 @@ func (r *Registry) ByType(t string) []*Repo {
// Foundation repos come first, then modules, then products.
//
// ordered, err := reg.TopologicalOrder()
//
// Usage: TopologicalOrder(...)
func (r *Registry) TopologicalOrder() ([]*Repo, error) {
// Build dependency graph
visited := make(map[string]bool)
@ -331,11 +345,13 @@ func (r *Registry) TopologicalOrder() ([]*Repo, error) {
}
// Exists checks if the repo directory exists on disk.
// Usage: Exists(...)
func (repo *Repo) Exists() bool {
return repo.getMedium().IsDir(repo.Path)
}
// IsGitRepo checks if the repo directory contains a .git folder.
// Usage: IsGitRepo(...)
func (repo *Repo) IsGitRepo() bool {
gitPath := filepath.Join(repo.Path, ".git")
return repo.getMedium().IsDir(gitPath)

View file

@ -36,6 +36,7 @@ type AgentPolicy struct {
}
// DefaultWorkConfig returns sensible defaults for workspace sync.
// Usage: DefaultWorkConfig(...)
func DefaultWorkConfig() *WorkConfig {
return &WorkConfig{
Version: 1,
@ -56,6 +57,7 @@ func DefaultWorkConfig() *WorkConfig {
// LoadWorkConfig reads .core/work.yaml from the given workspace root directory.
// Returns defaults if the file does not exist.
// Usage: LoadWorkConfig(...)
func LoadWorkConfig(m io.Medium, root string) (*WorkConfig, error) {
path := filepath.Join(root, ".core", "work.yaml")
@ -77,6 +79,7 @@ func LoadWorkConfig(m io.Medium, root string) (*WorkConfig, error) {
}
// SaveWorkConfig writes .core/work.yaml to the given workspace root directory.
// Usage: SaveWorkConfig(...)
func SaveWorkConfig(m io.Medium, root string, wc *WorkConfig) error {
coreDir := filepath.Join(root, ".core")
if err := m.EnsureDir(coreDir); err != nil {
@ -97,6 +100,7 @@ func SaveWorkConfig(m io.Medium, root string, wc *WorkConfig) error {
}
// HasTrigger returns true if the given trigger name is in the triggers list.
// Usage: HasTrigger(...)
func (wc *WorkConfig) HasTrigger(name string) bool {
for _, t := range wc.Triggers {
if t == name {