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

`f7eccb6`·

Soph·2w 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

## Changes

4

- cmd/entire/cli
  
  - Mactivity_cmd.go+1/-1

- Aentireapi_client.go+93

- Aentireapi_client_test.go+50

- Mrecap.go+25/-6

```go
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)
		}
```

Mcmd/entire/cli/activity_cmd.go+1/-1

```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 ""
}
```

Acmd/entire/cli/entireapi_client.go+93

```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) {
			t.Parallel()
			if got := firstActiveRepoID(tt.mirrors); got != tt.want {
				t.Fatalf("firstActiveRepoID = %q, want %q", got, tt.want)
			}
		})
	}
}
```

Acmd/entire/cli/entireapi_client_test.go+50

```go
// newRecapClient returns the recap client and the value to pass as /me/recap's
// ?repo= (its team/contributors scope): the current repo's ULID when routed to
// an entire-api cell (which addresses repos by id), or its owner/repo slug on
// the data API (which addresses them by name). Empty when the current repo
// can't be resolved — recap then shows the personal side only.
//
// It prefers the caller's home entire-api cell (the shared client), falling back
// to the data API when the region has no cell yet (ErrNoCellForJurisdiction) or
// the caller isn't logged in — recap tolerates the latter, rendering and letting
// the server answer 401 rather than hard-failing. Every other failure surfaces.
func newRecapClient(ctx context.Context, insecureHTTP bool) (*api.Client, string, error) {
	if client, err := auth.NewEntireAPICellClient(ctx, insecureHTTP, nil); err == nil {
		return client, currentRepoID(ctx), nil
	} else if !errors.Is(err, auth.ErrNoCellForJurisdiction) {
		// Best-effort upgrade: fall back to the data API on any cell failure. Its
		// path below tolerates a missing login (renders, lets the server 401), so
		// the not-logged-in case keeps working; log non-obvious failures.
		logging.Debug(ctx, "recap: entire-api cell client unavailable, using data API", "error", err.Error())
	}

if insecureHTTP {
		auth.EnableInsecureHTTP()
	}
	return api.NewClient(token), nil
}
```

Mcmd/entire/cli/recap.go+25/-6
