Documentation
¶
Overview ¶
Package igcatalog is the REST client for the ig catalog server on a Praxis deployment (mounted at /ai-api/ig, org-scoped). It is the ONLY network client of that server: `praxis ig <verb>` calls here; the `ig` binary itself never learns servers exist — it reads the filesystem that `praxis ig sync` materializes.
The package mirrors the layout of internal/duties and internal/memory: typed structs track the server's response models, exported function vars give tests a seam to swap, and every transport call sets whatever headers the profile's Auth() returns — always Authorization: Bearer <token>, plus X-Facets-Username for facets-mode control-plane PATs (the server resolves the identity via auth_service.validate_user()).
Backend routes (all under /ai-api/ig, org-scoped):
GET /catalogs list the org's catalogs
GET /catalogs/claims?git=<url> names of catalogs claiming a repo
GET /catalogs/{c} one catalog's summary (404 if absent)
POST /catalogs/{c}/members/{m} publish one member (gzipped graph.json)
GET /catalogs/{c}/bundle assembled catalog as a gzipped tarball
POST /catalogs/{c}/manifest push the manifest text + stamps
GET /catalogs/{c}/manifest pull the manifest text + stamps
Index ¶
Constants ¶
This section is empty.
Variables ¶
var Claims = func(baseURL string, auth map[string]string, git string) ([]string, error) { if git == "" { return nil, fmt.Errorf("git url is required") } q := url.Values{} q.Set("git", git) env, err := doJSON[claimsResponse](baseURL, auth, http.MethodGet, apiPrefix+"/catalogs/claims?"+q.Encode(), nil) if err != nil { return nil, err } return env.Catalogs, nil }
Claims returns the names of catalogs that have a member whose canonical git URL matches git. Repo CI loops over these to know which catalogs to refresh after a push.
var DownloadBundle = func(baseURL string, auth map[string]string, catalog, ifNoneMatch string) (body []byte, etag string, notModified bool, err error) { if baseURL == "" { return nil, "", false, fmt.Errorf("baseURL is required") } if len(auth) == 0 { return nil, "", false, fmt.Errorf("token is required") } if catalog == "" { return nil, "", false, fmt.Errorf("catalog is required") } ctx, cancel := context.WithTimeout(context.Background(), bundleTimeout) defer cancel() full := strings.TrimRight(baseURL, "/") + apiPrefix + "/catalogs/" + url.PathEscape(catalog) + "/bundle" req, err := http.NewRequestWithContext(ctx, http.MethodGet, full, nil) if err != nil { return nil, "", false, err } for k, v := range auth { req.Header.Set(k, v) } if ifNoneMatch != "" { req.Header.Set("If-None-Match", quoteETag(ifNoneMatch)) } client := httpclient.New(bundleTimeout) resp, err := client.Do(req) if err != nil { return nil, "", false, err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode == http.StatusNotModified { return nil, unquoteETag(resp.Header.Get("ETag")), true, nil } raw, err := io.ReadAll(resp.Body) if err != nil { return nil, "", false, fmt.Errorf("read body: %w", err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, "", false, fmt.Errorf("HTTP %d from %s: %s", resp.StatusCode, full, truncate(string(raw), 200)) } return raw, unquoteETag(resp.Header.Get("ETag")), false, nil }
DownloadBundle fetches the assembled catalog as a gzipped tarball. ifNoneMatch is the caller's last-synced digest; when it matches the server's current ETag the server returns 304 and this reports notModified=true with an empty body (a cheap no-op re-sync). On 200 it returns the tarball bytes and the ETag (the new digest).
var GetCatalog = func(baseURL string, auth map[string]string, name string) (*Catalog, error) { if name == "" { return nil, fmt.Errorf("catalog name is required") } c, err := doJSON[Catalog](baseURL, auth, http.MethodGet, apiPrefix+"/catalogs/"+url.PathEscape(name), nil) if err != nil { return nil, err } return &c, nil }
GetCatalog returns one catalog's summary. The server 404s when the catalog is absent; that surfaces as an `HTTP 404 …` error.
var ListCatalogs = func(baseURL string, auth map[string]string) ([]Catalog, error) { return doJSON[[]Catalog](baseURL, auth, http.MethodGet, apiPrefix+"/catalogs", nil) }
ListCatalogs returns every catalog in the org.
var ManifestPull = func(baseURL string, auth map[string]string, catalog string) (*Manifest, error) { if catalog == "" { return nil, fmt.Errorf("catalog is required") } m, err := doJSON[Manifest](baseURL, auth, http.MethodGet, apiPrefix+"/catalogs/"+url.PathEscape(catalog)+"/manifest", nil) if err != nil { return nil, err } return &m, nil }
ManifestPull fetches the served manifest (text + stamps) so a builder can diff it against their local copy.
var ManifestPush = func(baseURL string, auth map[string]string, catalog string, m Manifest) error { if catalog == "" { return fmt.Errorf("catalog is required") } body, err := json.Marshal(manifestPushRequest{Content: m.Content, GitSHA: m.GitSHA}) if err != nil { return err } path := apiPrefix + "/catalogs/" + url.PathEscape(catalog) + "/manifest" return sendBytes(baseURL, auth, http.MethodPost, path, "application/json", body) }
ManifestPush uploads the manifest text and its git sha. The server stamps pushed_by/pushed_at itself, so only content and git_sha go on the wire (the server's IgManifestPushRequest) — m.PushedBy/m.PushedAt are ignored here and exist only for the cmd layer's local echo.
var PublishMember = func(baseURL string, auth map[string]string, catalog, member string, gzGraph []byte, git, sha string) error { if catalog == "" || member == "" { return fmt.Errorf("catalog and member are required") } var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("graph", "graph.json.gz") if err != nil { return err } if _, err := part.Write(gzGraph); err != nil { return err } if git != "" { if err := writer.WriteField("git", git); err != nil { return err } } if sha != "" { if err := writer.WriteField("sha", sha); err != nil { return err } } if err := writer.Close(); err != nil { return err } path := apiPrefix + "/catalogs/" + url.PathEscape(catalog) + "/members/" + url.PathEscape(member) return sendBytes(baseURL, auth, http.MethodPost, path, writer.FormDataContentType(), body.Bytes()) }
PublishMember uploads one member's gzipped graph.json to a catalog, stamping the member's canonical git URL and commit sha. Server-side it is idempotent — republishing the same (git, sha) is accepted.
The server handler (publish_member) expects multipart/form-data: a file part named "graph" carrying the gzipped graph.json bytes, plus optional "git"/"sha" form fields. On the server those are Optional[...] = Form(None), so they are written only when non-empty; git/sha are NOT query parameters.
Functions ¶
This section is empty.
Types ¶
type Catalog ¶
type Catalog struct {
Name string `json:"name"`
Version string `json:"version"`
BuiltAt string `json:"built_at"`
Members []Member `json:"members"`
}
Catalog is the wire shape of a catalog summary: {name, version, built_at, members[]}. Only the fields the CLI surfaces are retained — encoding/json ignores the rest, so older binaries keep working as the server adds fields.
Version doubles as the bundle's ETag / the stored .sync.json digest: it is the single opaque token the server hands out to identify a built catalog. `praxis ig status` compares the local digest against this Version without downloading the bundle; `praxis ig sync` sends it as If-None-Match to get a cheap 304 when nothing changed.
type Manifest ¶
type Manifest struct {
Catalog string `json:"catalog,omitempty"`
Content string `json:"content"`
PushedBy string `json:"pushed_by,omitempty"`
PushedAt string `json:"pushed_at,omitempty"`
GitSHA string `json:"git_sha,omitempty"`
}
Manifest is the wire shape of a served manifest (the server's IgManifest): the catalog it belongs to, the text, plus the stamps the server records on push (who pushed it, when, and the git sha of the working tree the file came from). All four of Catalog/Content/PushedBy/PushedAt are required on the wire; GitSHA is nullable. Catalog is retained so the response type captures every field the server declares required, even though the CLI currently only surfaces Content.
type Member ¶
type Member struct {
Name string `json:"name"`
Kind string `json:"kind,omitempty"`
Git string `json:"git,omitempty"`
SHA string `json:"sha,omitempty"`
}
Member is the wire shape of one catalog member, mirroring the entries in ig's own metadata.json: a name, a kind ("code" or "infra"), and — for code members — the canonical git URL and the built commit sha. The infra member carries no repo, so Git/SHA are absent or JSON null on the wire; both decode to the empty string. omitempty keeps `praxis ig list --json` output tidy for members without a repo.