cli: route activity/recap through the shared entire-api cell client · Entire

cli: route activity/recap through the shared entire-api cell client

077b747→main· Soph·1w ago·4 files·+169 added/-7 removed

Reconcile onto the cell-routing client that landed with the experts work (#1588) instead of a parallel mechanism. activity and recap call the /me/* endpoints entire-api serves, so they now go through auth.NewEntireAPICellClient (home-jurisdiction routing, target=nil) — one routing/token path across the CLI.

Cell routing is a best-effort upgrade: any failure building the cell client (no cell for the region, not logged in, discovery/exchange error) falls back to the data API, which also serves /me/* and yields the canonical auth errors — so existing users are unaffected until their region has a cell. recap keeps its render-through-401 behaviour via that fallback.

recap's team column still needs the repo ULID for /me/recap?repo=; currentRepoID resolves it best-effort from the mirror id (which entire-api treats as the repo_id), empty → personal recap only.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

Sessions

dc4fb6be0484View transcript

[?
Route CLI Commands to entire-apiClaude Code·2 steps](/content/gh/entireio/cli/session/473b5e55-d61f-4bc6-bcae-ab078e0d3cdd#timeline-dc4fb6be0484/index.html)

Changes

4

    55 unmodified lines

func runActivity(ctx context.Context, w, errW io.Writer) error {
        return runAuthenticatedDataAPI(ctx, errW, false, func(ctx context.Context, client *api.Client) error {
            return runAuthenticatedActivityAPI(ctx, errW, false, func(ctx context.Context, client *api.Client) error {
                // Non-interactive fallback: piped output or accessibility mode
                if !interactive.IsTerminalWriter(w) || IsAccessibleMode() {
                    return runActivityStatic(ctx, w, client)
    ```

### cmd/entire/cli/activity_cmd.go
```go
package cli

import (
    "context"
    "errors"
    "io"
    "strings"

"github.com/entireio/cli/cmd/entire/cli/api"
    "github.com/entireio/cli/cmd/entire/cli/auth"
    "github.com/entireio/cli/cmd/entire/cli/gitremote"
    "github.com/entireio/cli/cmd/entire/cli/logging"
    "github.com/entireio/cli/internal/coreapi"
)

// runAuthenticatedActivityAPI runs fn with an authenticated client for the
// activity/recap surface. It prefers the caller's home entire-api cell (the same
// shared client the experts commands use), which serves the /me/* endpoints
// these commands call.
//
// Cell routing is a best-effort upgrade: any failure building the cell client —
// the region has no cell yet (ErrNoCellForJurisdiction), not logged in, or a
// discovery/exchange error — falls back to the data API, which also serves
// /me/* and yields the canonical auth errors (e.g. the "not logged in" hint).
// This keeps the migration transparent and non-regressive; non-obvious
// fallbacks are logged for diagnosis. Both backends expose the same /me/* paths,
// so fn is agnostic to which client it receives.
func runAuthenticatedActivityAPI(ctx context.Context, errW io.Writer, insecureHTTP bool, fn func(context.Context, *api.Client) error) error {
    client, err := auth.NewEntireAPICellClient(ctx, insecureHTTP, nil)
    if err != nil {
        if !errors.Is(err, auth.ErrNoCellForJurisdiction) {
            logging.Debug(ctx, "activity/recap: entire-api cell client unavailable, using data API", "error", err.Error())
        }
        return runAuthenticatedDataAPI(ctx, errW, insecureHTTP, fn)
    }
    return fn(ctx, client)
}

// forgeToMirrorProvider maps a gitremote forge identifier (e.g. "gh") to the
// upstream provider the control plane records mirrors under (e.g. "github").
// entire-api routing only supports GitHub mirrors today.
func forgeToMirrorProvider(forge string) (string, bool) {
    switch strings.ToLower(strings.TrimSpace(forge)) {
    case "gh", mirrorCloneProviderGitHub:
        return mirrorCloneProviderGitHub, true
    default:
        return "", false
    }
}

// currentRepoID best-effort resolves the current repo (its "origin" remote) to
// the ULID entire-api uses for repo-scoped params — recap's /me/recap?repo=.
// entire.io/api documents the mirror id as exactly that repo_id (repo_id =
// mirror_repos.id), and the CLI already lists mirrors via the control plane, so
// no extra resolution is needed. Any failure returns "" — recap then shows the
// personal side only rather than erroring.
func currentRepoID(ctx context.Context) string {
    forge, owner, repo, err := gitremote.ResolveRemoteRepo(ctx, "origin")
    if err != nil {
        return ""
    }
    provider, ok := forgeToMirrorProvider(forge)
    if !ok {
        return ""
    }
    c, err := coreapi.New()
    if err != nil {
        return ""
    }
    mirrors, err := listMirrorsForRepo(ctx, c, provider, strings.ToLower(owner), repo)
    if err != nil {
        return ""
    }
    return firstActiveRepoID(mirrors)
}

// firstActiveRepoID returns the id of the repo's first active mirror (the repo
// id is stable across a repo's placements, so any active one serves). Archived
// and failed/suspended placements are skipped — they can't answer for the repo.
func firstActiveRepoID(mirrors []coreapi.Mirror) string {
    for i := range mirrors {
        if mirrors[i].IsArchived.Or(false) {
            continue
        }
        if st := mirrors[i].Status.Or(coreapi.MirrorStatusReady); st == coreapi.MirrorStatusFailed || st == coreapi.MirrorStatusSuspended {
            continue
        }
        if id := strings.TrimSpace(mirrors[i].MirrorId); id != "" {
            return id
        }
    }
    return ""
}

cmd/entire/cli/entireapi_client.go

package cli

import (
    "testing"

"github.com/entireio/cli/internal/coreapi"
)

func TestForgeToMirrorProvider(t *testing.T) {
    t.Parallel()

for _, forge := range []string{"gh", "github", "GitHub", " gh "} {
        if p, ok := forgeToMirrorProvider(forge); !ok || p != mirrorCloneProviderGitHub {
            t.Errorf("forgeToMirrorProvider(%q) = (%q, %v), want (%q, true)", forge, p, ok, mirrorCloneProviderGitHub)
        }
    }
    if _, ok := forgeToMirrorProvider("gitlab"); ok {
        t.Error("forgeToMirrorProvider(gitlab) = ok, want not ok")
    }
}

func TestFirstActiveRepoID(t *testing.T) {
    t.Parallel()

archived := coreapi.Mirror{MirrorId: "archived", IsArchived: coreapi.NewOptBool(true)}
    failed := coreapi.Mirror{MirrorId: "failed", Status: coreapi.NewOptMirrorStatus(coreapi.MirrorStatusFailed)}
    suspended := coreapi.Mirror{MirrorId: "suspended", Status: coreapi.NewOptMirrorStatus(coreapi.MirrorStatusSuspended)}
    ready := coreapi.Mirror{MirrorId: "ready-ulid", Status: coreapi.NewOptMirrorStatus(coreapi.MirrorStatusReady)}
    unset := coreapi.Mirror{MirrorId: "unset-status-ulid"} // no status → treated as active

tests := []struct {
        name    string
        mirrors []coreapi.Mirror
        want    string
    }{
        {"none", nil, ""},
        {"single ready", []coreapi.Mirror{ready}, "ready-ulid"},
        {"unset status counts as active", []coreapi.Mirror{unset}, "unset-status-ulid"},
        {"skips archived and unhealthy", []coreapi.Mirror{archived, failed, suspended, ready}, "ready-ulid"},
        {"all inactive", []coreapi.Mirror{archived, failed, suspended}, ""},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := firstActiveRepoID(tt.mirrors); got != tt.want {
                t.Fatalf("firstActiveRepoID = %q, want %q", got, tt.want)
            }
        })
    }
}

cmd/entire/cli/entireapi_client_test.go

// Test code omitted for brevity

cmd/entire/cli/recap.go

// Test code omitted for brevity