Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,26 @@ Top-level lifecycle and standalone commands: `enable`, `disable`, `status`,
`login`, `logout`, `clean`, `version`, `dispatch`, `activity`, `help`,
`configure`, `agent-help`, `api`.

`status` also reports whether the current clone points at an Entire mirror. The
"is it a mirror, and which cluster" half is read offline from the clone's git
remote (an `entire://` URL); the mirror's live state (processing / ready /
failed / suspended) is a best-effort, time-bounded control-plane lookup done
only when a mirror remote is present *and* the caller is logged in — logged out,
the state shows `unknown` with an `entire login` hint, and an ordinary
(non-mirror) clone triggers no network at all. When the clone pulls directly
from a mirrorable forge remote instead of a mirror, the human output prints a
hint pointing at `entire repo mirror use` — which repoints the clone at an
existing mirror, or tells the user to `mirror create` when there is none. The
forge host named in the hint is read from the remote URL (so it is the real
provider, e.g. `github.com`, not a hardcoded string), and the trigger gates on
`gitremote.IsSupportedForge`, so it widens automatically if more forges become
mirrorable (only GitHub today). The hint deliberately describes only the local
fact (this clone isn't using a mirror); it does not claim the repo has no
mirror, because that is a server-side fact `status` does not look up. Surfaced in the human output (short
and `--detailed`) and as the `mirror` object in `status --json` (`null` when the
clone isn't pointed at a mirror — the hint is human-output only). See
`status_mirror.go`.

`api` is an authenticated passthrough to Entire's HTTP APIs (gh-style): it
attaches the right bearer and dials the right host so callers don't plumb auth
themselves. `--to core` (default) hits the control plane; `--to cell` hits an
Expand Down
21 changes: 21 additions & 0 deletions cmd/entire/cli/gitremote/gitremote.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,27 @@ func GetRemoteURL(ctx context.Context, remoteName string) (string, error) {
return GetRemoteURLInDir(ctx, "", remoteName)
}

// ListRemotesInDir returns the names of every git remote configured in dir
// (dir "" means the current working directory). An empty slice, not an error,
// is returned when the repo has no remotes.
func ListRemotesInDir(ctx context.Context, dir string) ([]string, error) {
cmd := exec.CommandContext(ctx, "git", "remote")
if dir != "" {
cmd.Dir = dir
}
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("list git remotes: %w", err)
}
var names []string
for line := range strings.SplitSeq(strings.TrimSpace(string(output)), "\n") {
if name := strings.TrimSpace(line); name != "" {
names = append(names, name)
}
}
return names, nil
}

// GetRemoteURLInDir returns the URL configured for the named git remote in dir.
func GetRemoteURLInDir(ctx context.Context, dir, remoteName string) (string, error) {
cmd := exec.CommandContext(ctx, "git", "remote", "get-url", remoteName)
Expand Down
30 changes: 30 additions & 0 deletions cmd/entire/cli/gitremote/gitremote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,33 @@ func TestResolveRemoteRepo_MissingRemote(t *testing.T) {
_, _, _, err := ResolveRemoteRepo(context.Background(), "origin")
assert.Error(t, err)
}

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

t.Run("returns configured remotes", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
testutil.InitRepo(t, dir)
for name, url := range map[string]string{
"origin": "git@github.com:octocat/hello-world.git",
"upstream": "https://github.com/octocat/hello-world",
} {
cmd := exec.CommandContext(t.Context(), "git", "remote", "add", name, url)
cmd.Dir = dir
require.NoError(t, cmd.Run())
}
got, err := ListRemotesInDir(t.Context(), dir)
require.NoError(t, err)
require.ElementsMatch(t, []string{"origin", "upstream"}, got)
})

t.Run("empty for a repo with no remotes", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
testutil.InitRepo(t, dir)
got, err := ListRemotesInDir(t.Context(), dir)
require.NoError(t, err)
require.Empty(t, got)
})
}
18 changes: 13 additions & 5 deletions cmd/entire/cli/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ func runStatus(ctx context.Context, w io.Writer, detailed, jsonOutput bool) erro
}

// Check if we're in a git repository
if _, repoErr := paths.WorktreeRoot(ctx); repoErr != nil {
repoRoot, repoErr := paths.WorktreeRoot(ctx)
if repoErr != nil {
fmt.Fprintln(w, "✕ not a git repository")
return nil //nolint:nilerr // Not being in a git repo is a valid status, not an error
}
Expand Down Expand Up @@ -91,7 +92,7 @@ func runStatus(ctx context.Context, w io.Writer, detailed, jsonOutput bool) erro
sty := newStatusStyles(w)

if detailed {
return runStatusDetailed(ctx, w, sty, settingsPath, localSettingsPath, projectExists, localExists)
return runStatusDetailed(ctx, w, sty, repoRoot, settingsPath, localSettingsPath, projectExists, localExists)
}

// Short output: just show the effective/merged state
Expand All @@ -104,6 +105,7 @@ func runStatus(ctx context.Context, w io.Writer, detailed, jsonOutput bool) erro
if s.Enabled {
writeActiveSessions(ctx, w, sty)
}
writeMirrorStatus(ctx, w, repoRoot, sty)
writeAgentHelpHint(w, sty)

return nil
Expand All @@ -124,7 +126,7 @@ func writeAgentHelpHint(w io.Writer, sty statusStyles) {
}

// runStatusDetailed shows the effective status plus detailed status for each settings file.
func runStatusDetailed(ctx context.Context, w io.Writer, sty statusStyles, settingsPath, localSettingsPath string, projectExists, localExists bool) error {
func runStatusDetailed(ctx context.Context, w io.Writer, sty statusStyles, repoRoot, settingsPath, localSettingsPath string, projectExists, localExists bool) error {
// First show the effective/merged status
effectiveSettings, err := LoadEntireSettings(ctx)
if err != nil {
Expand Down Expand Up @@ -154,6 +156,7 @@ func runStatusDetailed(ctx context.Context, w io.Writer, sty statusStyles, setti
if effectiveSettings.Enabled {
writeActiveSessions(ctx, w, sty)
}
writeMirrorStatus(ctx, w, repoRoot, sty)
writeAgentHelpHint(w, sty)

return nil
Expand Down Expand Up @@ -600,7 +603,10 @@ type statusJSON struct {
// HooksOutdated lists agents whose installed hook config is out of date and
// should be refreshed with `entire enable --force`.
HooksOutdated []string `json:"hooks_outdated,omitempty"`
Error string `json:"error,omitempty"`
// Mirror describes the Entire mirror this clone's git remote points at, or is
// omitted when the clone targets the forge directly.
Mirror *mirrorJSON `json:"mirror,omitempty"`
Error string `json:"error,omitempty"`
}

type sessionBriefJSON struct {
Expand All @@ -614,7 +620,8 @@ func runStatusJSON(ctx context.Context, w io.Writer) error {
return json.NewEncoder(w).Encode(v)
}

if _, err := paths.WorktreeRoot(ctx); err != nil {
repoRoot, err := paths.WorktreeRoot(ctx)
if err != nil {
return writeJSON(statusJSON{Error: "not a git repository"})
}

Expand Down Expand Up @@ -650,6 +657,7 @@ func runStatusJSON(ctx context.Context, w io.Writer) error {
Agents: []string{},
ActiveSessions: []sessionBriefJSON{},
AgentHelp: agentHelpCommand,
Mirror: mirrorStatusJSON(ctx, repoRoot),
}

if s.Enabled {
Expand Down
Loading
Loading