chore(ax): add usage docs to exported APIs
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
dd59b177c6
commit
a0fac1341b
65 changed files with 298 additions and 0 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 "*****"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
12
git/git.go
12
git/git.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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++
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue