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
- cmd/entire/cli
- Mactivity_cmd.go+1/-1
- Aentireapi_client.go+93
- Aentireapi_client_test.go+50
- Mrecap.go+25/-6
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