package gitea import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" "sgg-ai-skill-manager/internal/domain" ) const ( AuthPassword = "password" AuthToken = "token" ) type Auth struct { Type string Username string Secret string } type Client struct { baseURL string auth Auth httpClient *http.Client } type apiRepo struct { Name string `json:"name"` FullName string `json:"full_name"` Description string `json:"description"` CloneURL string `json:"clone_url"` SSHURL string `json:"ssh_url"` DefaultBranch string `json:"default_branch"` UpdatedAt string `json:"updated_at"` } type apiUser struct { UserName string `json:"username"` } func NewClient(baseURL string, auth Auth) *Client { return &Client{ baseURL: strings.TrimRight(baseURL, "/"), auth: auth, httpClient: &http.Client{Timeout: 20 * time.Second}, } } func (c *Client) ListOrgSkills(ctx context.Context, org string) ([]domain.RemoteSkill, error) { var skills []domain.RemoteSkill for page := 1; ; page++ { var repos []apiRepo endpoint := fmt.Sprintf("/api/v1/orgs/%s/repos?page=%d&limit=50", url.PathEscape(org), page) if err := c.getJSON(ctx, endpoint, &repos); err != nil { return nil, err } if len(repos) == 0 { break } for _, repo := range repos { hasSkill, err := c.HasRootSkill(ctx, org, repo.Name, repo.DefaultBranch) if err != nil { skills = append(skills, domain.RemoteSkill{ Name: repo.Name, FullName: repo.FullName, Status: "check_failed", Error: err.Error(), }) continue } if !hasSkill { continue } skills = append(skills, domain.RemoteSkill{ Name: repo.Name, FullName: repo.FullName, Description: repo.Description, CloneURL: repo.CloneURL, SSHURL: repo.SSHURL, DefaultBranch: repo.DefaultBranch, UpdatedAt: repo.UpdatedAt, Status: "remote", }) } if len(repos) < 50 { break } } return skills, nil } func (c *Client) HasRootSkill(ctx context.Context, org, repo, ref string) (bool, error) { endpoint := fmt.Sprintf( "/api/v1/repos/%s/%s/contents/SKILL.md", url.PathEscape(org), url.PathEscape(repo), ) if ref != "" { endpoint += "?ref=" + url.QueryEscape(ref) } req, err := c.newRequest(ctx, http.MethodGet, endpoint) if err != nil { return false, err } resp, err := c.httpClient.Do(req) if err != nil { return false, err } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return false, nil } if resp.StatusCode >= 200 && resp.StatusCode < 300 { return true, nil } return false, fmt.Errorf("check SKILL.md failed: %s", resp.Status) } func (c *Client) TestConnection(ctx context.Context, org string) (domain.TestConnectionResult, error) { var user apiUser if err := c.getJSON(ctx, "/api/v1/user", &user); err != nil { return domain.TestConnectionResult{}, err } req, err := c.newRequest(ctx, http.MethodGet, "/api/v1/orgs/"+url.PathEscape(org)) if err != nil { return domain.TestConnectionResult{}, err } resp, err := c.httpClient.Do(req) if err != nil { return domain.TestConnectionResult{}, err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return domain.TestConnectionResult{}, fmt.Errorf("org access failed: %s", resp.Status) } return domain.TestConnectionResult{ OK: true, Message: "connected", Username: user.UserName, Org: org, }, nil } func (c *Client) getJSON(ctx context.Context, endpoint string, target any) error { req, err := c.newRequest(ctx, http.MethodGet, endpoint) if err != nil { return err } resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("gitea request failed: %s", resp.Status) } return json.NewDecoder(resp.Body).Decode(target) } func (c *Client) newRequest(ctx context.Context, method, endpoint string) (*http.Request, error) { req, err := http.NewRequestWithContext(ctx, method, c.baseURL+endpoint, nil) if err != nil { return nil, err } if c.auth.Type == AuthToken { req.Header.Set("Authorization", "token "+c.auth.Secret) } else if c.auth.Username != "" || c.auth.Secret != "" { req.SetBasicAuth(c.auth.Username, c.auth.Secret) } req.Header.Set("Accept", "application/json") return req, nil }