Interactive Repo Mirror Onboarding Wizard

take a look at the current machinery we provide in entire repo mirror .... i want to provide a new, smooth, onboarding UX. What i'd like is:
- a new
entire repo mirror createcommand (when no arguments are provided)
it should
- ensure the user has a valid auth context (like
entire auth statusdoes), then - use the existing
entire repo mirror list --show-availablemachinery to show an interactive TUI picker to allow enabling/disabling a number of repos - once the user commits that selection, a subsequent TUI picker suggesting they mirror the repo in 1..m in any of our available regions (open question: how do we find known cells?)
- kick off the mirror creation in parallel and poll for success. once all are successfull, return and list the clone URLs.
ask me anything, validate all assumptions in code, don't assume things. let's cook up a plan. all the ingredients should already be in the repo.
You'd like to clarify the questions first. What would you like to dig into or add context on?
To help focus it, here's where I currently have open questions — tell me which of these (or something else) you want to discuss:
- Region/cell discovery — this is the real blocker. There's genuinely no list source in the repo. Do you happen to know of a cluster-enumeration API on the core side that the OpenAPI spec just doesn't model yet? Or a config/env mechanism you had in mind? If not, we're choosing between a hardcoded list, free-text entry, or gating the feature on new backend work.
- Region × repo fan-out — whether "mirror in 1..m regions" means a cross-product (every selected repo into every selected region) or something narrower.
- Picker scope — whether the repo multi-select should reflect already-mirrored state (enable/disable framing) or just offer new repos.
What's on your mind?
I am getting another agent to build a new entire-core api as we speak:
Expose a trimmed public /api/v1/clusters (slug, jurisdiction, public_url, is_default)
Done. Wizard implemented, tested, lint-clean, full unit suite green.
Bottom line
entire repo mirror create with no args now launches an interactive onboarding wizard; the positional-arg form is unchanged. Everything is real and final except region enumeration, which sits behind a one-function seam waiting on your GET /api/v1/clusters.
What shipped
cmd/entire/cli/repo_mirror_create_wizard.go(new) — auth gate (reusesresolveAuthStatusTarget+/meprobe, with re-login hints) → repo multi-select (ListAvailableMirrors, filtered to available + writable) → region multi-select (default pre-checked) → cross-productCreateMirrorfan-out (boundederrgroup, one cluster client per region) → per-mirror clone poll (waitForMirrorClonetoio.Discardso concurrent heartbeats don't interleave; aggregate spinner instead) → results table + copy-pastegit cloneblock.cmd/entire/cli/repo_mirror.go—createArgsRangeArgs(0,2), dispatches to the wizard on zero args; help/comment updates.*_wizard_test.go(new) — table tests for all pure helpers.
Design points worth noting
- Cross-product, available-only, regenerate-when-spec-lands — per your three answers.
- Partial success: a region the active login can't reach fails only that region's pairs; the run continues and the table shows what succeeded. Non-empty failure set → non-zero exit via
SilentError(table still prints). - Security:
hostFromPublicURLrejects thehost@evil.comuserinfo trick and trailing paths before they reach clone URLs / STS audience — same posture as the existingvalidateClusterHost.
The one gate
availableRegions() returns only defaultClusterHost today. When your /api/v1/clusters lands: curl the spec → go generate ./internal/coreapi/... → swap that function's body to client.ListClusters(...) mapping public_url through hostFromPublicURL. The regionChoice struct already carries slug/jurisdiction/host/is_default, so the swap is local to that function (TODO(clusters-api) marks it). Saved to memory for a future session.
I skipped speculative doc edits about /api/v1/clusters — documenting an endpoint that doesn't exist yet would mislead; the code TODO is the source of truth. Nothing committed or pushed.
Two separate things both called "status" — neither is invented by the wizard locally except the second:
1. The repo-picker status (available / mirrored / owner-only) — server-side, not the CLI.
- Comes from
GET /mirrors/available→AvailableMirror.Status, fetched byclient.ListAvailableMirrors(...)inrunMirrorCreateWizard. - The server computes it live from GitHub against the caller's login (spec: "available (can onboard), mirrored (already mirrored), or owner-only").
- The CLI only consumes it —
selectableAvailableRepos(repo_mirror_create_wizard.go) filters toStatus == available && Access ∈ {write, admin}. It never derives the value. - ⚠️ This field is cluster-agnostic: "mirrored" means mirrored somewhere, not in any specific region. So the picker can't tell you "already mirrored in us-east." That's the limitation I flagged in the plan, and why re-selecting a (repo, region) that already exists is safe —
CreateMirroris idempotent and the wizard reports it as already-created.
2. The results-table STATUS column (ready / registered / empty / suspended / timed out / error) — computed client-side, in the wizard.
- Set in
createOneMirrorfrom two signals: theCreateMirrorresponse (created.Empty) and the clone-poll outcome (waitForMirrorClonereturning nil /auth.ErrRepoTargetUnknown/context.DeadlineExceeded/ other). - These are local labels (the
mirrorStatus*constants), not a server field.
Which one were you asking about?
commit everything now. first fixup for this feature work, make use of this new API: https://github.com/entirehq/entiredb/commit/ce85f820363b36c5c6cd9f7b6cf839564d1a1221
Committed.
Bottom line
Wired the wizard to the real GET /api/v1/clusters API and committed everything as one commit (e9e7c0226c). Not pushed.
What the fixup added
availableRegionsnow callscoreapi.Client.ListClusters(was thedefaultClusterHostseam). Pure mapping extracted toclustersToRegions, which drops any cluster whosepublicUrlfails thehostFromPublicURLsafety check.- Regenerated the ogen client — got
ListClusters() (*ListClustersOutputBody, error)andCluster{Slug, Jurisdiction, PublicUrl, IsDefault}, exactly matching the entiredb commit's contract. - Swapped the obsolete seam test for
TestClustersToRegions; cleaned up the now-stale comments.
The one caveat (recorded in memory)
The endpoint isn't deployed to us.console.entire.io yet, and entiredb's spec is code-first (huma, no committed openapi.json). So I hand-authored the /clusters path + schemas into internal/coreapi/spec/core.openapi.json (modeled on /mirrors/available) and ran go generate. Once the endpoint ships, the normal curl … -o core.openapi.json + regenerate is idempotent and replaces my fragment with the live one — verify field names still match if you see a /clusters diff then.
Verification
go build ./...,mise run lint(0 issues, clean tree after fmt),mise run test(6710 pass) — all green.
One thing I did not do: run E2E tests (they make real API calls / cost money, and the endpoint isn't live anyway). Let me know if you want me to push.
i'd like the user to be shown a "Fetching available repos ..." type status indicator, then be presented with the select-mirrors menu.
Done. Both pre-picker fetches now show a status line.
Bottom line
runMirrorCreateWizard now wraps each network fetch in the existing startSpinner helper:
- "Fetching available repos" before the repo picker (as you asked).
- "Fetching regions" before the region picker — same pause-before-menu gap, so I added it for consistency.
On a TTY it animates, then resolves to ✓ Fetching available repos and the menu appears; on failure the line is erased. On non-terminals (pipes/tests) it's a no-op except the success checkmark line — same behavior as entire explain's "Loading checkpoints".
Build, lint (0 issues), and tests pass. Not committed — say the word and I'll add it as a new commit on top of e9e7c0226c.
Done. Verified and recorded — nothing to commit (working tree clean, the committed client already matches the now-live API).
let's put this work onto a branch named to attach it to https://linear.app/entirehq/issue/COR-645/cli-smooth-mirror-onboarding-flow.
Done. Branch renamed to cor-645-cli-smooth-mirror-onboarding-flow with both commits intact.
- The
cor-645in the name is what Linear matches on, so it'll auto-attach to COR-645 once you push. - Not pushed (per your usual flow). When you push, use
git push -u origin cor-645-cli-smooth-mirror-onboarding-flow— Linear picks it up from the branch name on the PR/push.
let's switch our base branch to https://github.com/entireio/cli/pull/1518, and notice that it includes a new field in mirror status. i want us to use that endpoint to poll for "when is the mirror ready" rather than what we currently do, which is some kind of info/refs request. make sure we fix both this new smooth flow as well as the one-shot command entire repo mirror create github.com/octocat/hello-world eu-west-1.entire.io (they should use the exact same code path in fact)
Done.
Bottom line
Branch rebased onto PR #1518 and both create paths now poll the new Mirror.status endpoint for clone readiness via one shared code path. All committed (not pushed).
What changed
- Rebased
cor-645-cli-smooth-mirror-onboarding-flowontoorigin/chore/refresh-openapi-spec(#1518). Our hand-authored/clustersspec fragment was subsumed by the PR's full refresh, so I took the PR'sinternal/coreapiwholesale — our commit is now just the wizard code. - Shared readiness path: new
createAndAwaitMirror→awaitMirrorReadypollsGetMirrorforMirror.status(processing/ready/failed/suspended). Bothentire repo mirror create <url> <cluster>and the wizard's per-mirrorcreateOneMirrorcall it — verified both route through the same function. - Deleted the old info/refs probe machinery (
waitForMirrorClone,mirrorAdvertisesHead,checkProbeRedirect, the probe HTTP client + repo-token source) andfinishMirrorCreate. Suspension/failure now come straight from the control plane — no repo-scoped token exchange or data-plane round trip.auth.RepoScopedTokenstays (still used by the git transport). - Tests overhauled: removed the probe tests; added
TestAwaitMirrorReady(fake getter: ready/failed/suspended/timeout) andTestReportOneShotMirror(all lifecycle presentations).
Verification
go build ./...,mise run lint(0 issues),mise run test(6777 pass) — all green.- The LSP diagnostics you may see (
coreapi.MirrorStatus undefined) are stale — the rebased generated client has them; real build/lint/test confirm.
Commits on the branch (over #1518)
993f948bfwizard +d73f27a28spinners +90dea374fstatus-poll refactor.
Note: PR #1518's spec refresh also pulled in unrelated breaking changes (ListOrgs params, typed grant roles) — already adapted on that branch, so it builds. When #1518 merges to main, this branch's base collapses cleanly.
mg produced no output (unknown command or a no-op alias). Nothing to do on my end — let me know what's next.
when i run just 'entire repo mirror create github.com/octocat/hello-world', we should use the default cluster which we fetch from the new list clusters API this PR uses, too.
Done. Committed as 289a796c8.
Bottom line
entire repo mirror create github.com/octocat/hello-world (no cluster-host) now fetches the default cluster from the same GET /api/v1/clusters API the wizard uses, instead of the hardcoded constant.
What changed
- When
[cluster-host]is omitted, the one-shot resolves it viaresolveDefaultClusterHost→availableRegions(ListClusters): picks theis_defaultcluster, or the sole cluster if there's only one. It printsUsing default cluster <host>and errors with "pass [cluster-host] explicitly" when the catalog is empty or ambiguous. - The catalog lookup runs through
runCore(active context), then the create runs throughrunCoreForClusterfor the resolved host — so--insecure-http-authand error mapping are handled. - When
[cluster-host]is given, behavior is unchanged (validate + use directly). - Updated the
createhelp text; addedTestPickDefaultRegionHost(pure selection logic).
Note: remove/collaborators still use the hardcoded defaultClusterHost via clusterArg — you only asked about create, so I left those. Say the word if you want remove to default the same way.
Verification
mise run lint(0 issues),mise run test(6782 pass), build OK — all green.
Branch cor-645-cli-smooth-mirror-onboarding-flow now has 4 commits over #1518. Not pushed.
i want to cherry-pick bd0a96b0b495ba5a727563530df127d5f0c4e9d5, because we should not show the user fake/invalid example cluster URLs. ensure we only use aws-us-east-2.entire.io as cluster host example anywhere user-facing.
also, give me the curl command to list clusters too. might need to dump a jwt somehow.
Everything's in place. Here's the summary and your curl recipe.
Cherry-pick + examples — done
- Cherry-picked
bd0a96b0b("Fix region FQDNs") as9de7d632c, resolving both conflicts to useaws-us-east-2.entire.io. - Swept all non-test code:
aws-us-east-2.entire.iois now the only cluster-host example in user-facing strings (thecreateandcollaborators addexamples that previously saideu-west-1.entire.io/us-east-2.entire.ioare fixed). Remaining.entire.ioliterals in code are justdocs.entire.ioURLs. Tests still useeu-west-1.entire.ioetc., but those aren't user-facing. - Lint clean (0 issues), 6782 tests pass.
curl the clusters API
There's no entire print-token command — the login JWT sits in the macOS keychain as "<JWT>|<expiry>", keyed by your active context's keychain_service + handle. Your active context is eu.auth.entire.io, so the endpoint is https://eu.auth.entire.io/api/v1/clusters.
For your current setup that resolves to CORE=https://eu.auth.entire.io.
Caveats:
security ... -wmay pop a keychain-access prompt the first time — allow it. (I deliberately didn't run it here so your JWT doesn't land in this transcript.)- If the token's expired and that context has no refresh token, step 1 won't save it and you'll get a 401 →
entire loginfirst. - Note this dials the EU core (your active context). The spec I refreshed earlier was from
us.console.entire.io— different region; the EU core returns its own cluster list.
Want me to fold a hidden entire auth token (prints the active bearer to stdout) into this branch so curl-ing is a one-liner next time?
Want me to fold a hidden entire auth token (prints the active bearer to stdout) into this branch so curl-ing is a one-liner next time?
yes.
Done. Committed as 2b58593b8.
entire auth token — added (hidden)
Prints the active control-plane bearer to stdout. Honors ENTIRE_TOKEN verbatim, else resolves + refreshes the active context's login JWT (same bearer the API client uses). Errors and the not-logged-in hint go to stderr so stdout stays clean for $(...). Hidden from help (emits a live credential).
Your curl is now a one-liner
(That's your active EU context. For whatever's active, the core host is the active context's core_url.)
Verification
- New
TestAuthTokenCmd(env-token passthrough + not-logged-in SilentError) passes; lint 0 issues; full suite green (6785 tests). - Updated CLAUDE.md's
authsurface to list the hiddentokensubcommand.
Branch cor-645-cli-smooth-mirror-onboarding-flow now has 6 commits over #1518. Not pushed.
ok here is what i'm seeing, this isn't right:
12:33:56 (git) cor-645-cli-smooth-mirror-onboarding-flow $ curl -fsS https://eu.auth.entire.io/api/v1/clusters
-H "Authorization: Bearer $(entire auth token)" | jq .
{
"$schema": "https://eu.auth.entire.io/api/v1/schemas/ListClustersOutputBody.json",
"clusters": [
{
"slug": "aws-ap-southeast-2",
"jurisdiction": "au",
"publicUrl": "https://aws-ap-southeast-2.entire.io",
"isDefault": true
},
{
"slug": "aws-eu-central-1",
"jurisdiction": "eu",
"publicUrl": "https://aws-eu-central-1.entire.io",
"isDefault": true
},
{
"slug": "aws-us-east-2",
"jurisdiction": "us",
"publicUrl": "https://aws-us-east-2.entire.io",
"isDefault": true
}
]
}
[paul@boulot] ~/.claude/worktrees/cli/mutable-spinning-umbrella
12:34:49 (git) cor-645-cli-smooth-mirror-onboarding-flow $ entire repo mirror create
Signed in as toothbrush via https://eu.auth.entire.io
✓ Fetching available repos
✓ Fetching regions
┃ Select regions to mirror into
┃ Each selected repo is mirrored into every selected region. Space to select, enter to confirm.
┃ > [•] aws-ap-southeast-2 (au)
x toggle • ↑ up • ↓ down • / filter • enter submit • ctrl+a select none
Not a bug — it's working as designed. isDefault means "default within its jurisdiction," not "the one global default." Each jurisdiction (au/eu/us) has exactly one default cluster, so the global list correctly shows three trues.
given this, let's make sure we offer the user to create a mirror on each of those clusters. also, when retrieving the default when creating a mirror, let's make sure we grab the default for our current auth context's jurisdiction.
Done. Committed as 6f7d46408.
What changed
isDefault is per-jurisdiction (au/eu/us each have one), so "first default wins" was wrong. Now the caller's jurisdiction comes from /me (GetMeOutputBody.Jurisdiction) and drives both paths:
- One-shot
entire repo mirror create <url>(no cluster-host): picks theisDefaultcluster whosejurisdictionmatches yours — for youreucontext that'saws-eu-central-1.entire.io, not the AU one that happened to sort first. - Wizard: still lists every cluster (you can mirror into any/all — "offer each"), but pre-selects only your jurisdiction's default instead of all three. The "Signed in as" line now shows the jurisdiction:
Signed in as toothbrush (eu) via ….
Both error with "pass [cluster-host] explicitly" when the jurisdiction is unknown or has no default. Unknown-jurisdiction fallbacks (lone cluster / lone default) are preserved.
Verification
mise run lint(0 issues),mise run test(6787 pass), build OK.- Updated
TestPickDefaultRegionHost(jurisdiction matching + fallbacks) andTestClusterChoices(eu pre-selects only the eu default; unknown jurisdiction pre-selects nothing).
Branch now has 7 commits over #1518. Not pushed.
Note: I didn't apply jurisdiction-awareness to remove/collaborators (they still use the hardcoded defaultClusterHost via clusterArg) — out of scope for this ask, but easy to extend if you want consistency there.
I didn't apply jurisdiction-awareness to remove/collaborators (they still use the hardcoded defaultClusterHost via clusterArg)
explain what this implies
Bottom line: for any non-US user, omitting [cluster-host] on remove/collaborators silently targets the US cluster (aws-us-east-2.entire.io) — which is now not where create puts their mirrors. So the omit-the-arg workflow is inconsistent and will mostly 404 or act on the wrong placement.
Concretely, after this change the omitted-cluster default diverges by command:
create <url>(no cluster) → your jurisdiction's default (e.g.aws-eu-central-1for youreucontext).remove <url>(no cluster) → alwaysaws-us-east-2(hardcodeddefaultClusterHostviaclusterArg).collaborators add/remove/list <url> <handle>(no cluster) → alwaysaws-us-east-2(viaclusterArgAt).
What that implies for a non-US user who relies on defaults:
remove404s. They created the mirror onaws-eu-central-1;remove github.com/x/y(no cluster) looks for it onaws-us-east-2, finds nothing, and errors "no such mirror" even though the mirror exists. Confusing: create "worked" but remove "can't find it."collaboratorsoperate on the wrong/absent placement. Grants are per-cluster (a mirror is a per-cluster native repo with its own SpiceDB grant).collaborators add … --role writer(no cluster) grants on theaws-us-east-2placement, not the eu one the user actually has — so the grant either 404s or lands on a cluster they didn't mean.- Possible cross-jurisdiction auth error instead.
runCoreForCluster("aws-us-east-2.entire.io")discovers that cluster's cores; if the eu login has no context eligible for the US cluster, they get a "log in to …" error rather than a clean result. - Real (if narrow) footgun: a user with mirrors in both eu and us who runs
remove <url>expecting "the one I just made" (eu) would instead remove the us placement, if one exists. No mass data loss, but it's a wrong-target deletion.
What it does not affect:
- US users — the hardcoded default happens to equal their jurisdiction's default, so everything's consistent for them.
- Anyone passing
[cluster-host]explicitly — always correct; the positional arg overrides the default on every command. - The clone URL printed by
create/listalways names the real cluster, so users can see where a mirror actually lives.
The fix, if you want consistency, is small: route remove/collaborators' omitted-cluster case through the same resolveDefaultClusterHost (catalog + /me jurisdiction) instead of clusterArg/clusterArgAt. The reason I held off: those commands take an existing placement, so arguably they shouldn't guess at all — a case could be made to require [cluster-host] there (fail with "which cluster?" rather than default), since "default for create" and "default for operate-on-existing" aren't obviously the same intent.
Want me to (a) make them jurisdiction-aware like create, or (b) require an explicit cluster on remove/collaborators?
hm. i want this cluster-guessing to only apply to the interactive mirror creation flow - for the non-interactive one-shot ones, make sure we don't change the default behaviour. i.s., hardcoded defaultClusterHost
Done. Committed as 68e81f383.
Bottom line
Cluster-guessing is now confined to the interactive wizard. All non-interactive commands keep the fixed defaultClusterHost (aws-us-east-2.entire.io).
Final state per command (omitted [cluster-host])
create(no args) → interactive wizard, jurisdiction-aware: lists all clusters, pre-selects your jurisdiction's default. (kept)create <url>→aws-us-east-2.entire.io. (reverted to original)remove <url>,collaborators …→aws-us-east-2.entire.io. (unchanged)
So scripts get stable, predictable behavior, and the symmetry between create <url> / remove <url> / collaborators is restored (all share one fixed default).
What changed
- One-shot
createelsebranch reverted toclusterArg(args)+ validate. - Removed the now-unused
resolveDefaultClusterHost/callerJurisdiction/pickDefaultRegionHostandTestPickDefaultRegionHost. - Help text updated: "defaults to aws-us-east-2.entire.io when omitted (the interactive wizard … lets you pick clusters)."
- Kept: the wizard's jurisdiction-aware pre-selection,
/meJurisdictionplumbing, and the "Signed in as toothbrush (eu)" line.
Lint clean (0 issues), 6780 tests pass, help verified. Branch now has 8 commits over #1518; not pushed.
One note: commit 289a796c8 ("Default one-shot create cluster to the catalog default") is now effectively undone by this commit — the history shows both. If you'd rather it not appear as a flip-flop in the PR, say the word and I can squash those two; otherwise I'll leave history append-only per your usual preference.
why am i still only seeing one option here? [Image #2] is it because there are multiple isDefault==true?
Done — 37fa329a5.
Answer to your question
No, it's not the multiple isDefault==true. I proved it with your exact API response: the picker is built with all 3 options (regions=3, opts=3, aws-eu-central-1 pre-selected). isDefault only controls which rows start checked — it never removes rows.
You were seeing one because huh's region list is a scrollable viewport sized to your (short ~9-line) terminal: title + description + footer consumed the rows, so only the top option rendered and the other two were below the fold (press ↓, or use a taller window, to see them).
What I changed (37fa329a5)
- Caller's-jurisdiction clusters listed first, so on a cramped terminal the visible top row is your relevant, pre-checked default (
aws-eu-central-1 (eu)) instead ofaws-ap-southeast-2 (au). Other jurisdictions are still there, one ↓ away. - Trimmed the description ("Each repo is mirrored into every selected region.") to reclaim a row — the footer already shows toggle/submit.
Lint clean, 6780 tests pass. TestClusterChoices updated to assert the home-first ordering. Branch now has 9 commits over #1518; not pushed.
If you want the rest visible without scrolling regardless of terminal size, the only real lever is a taller terminal — huh can't render more option rows than the window has. I can also add a (3 regions — ↓ to see all) hint to the title if the below-the-fold case still trips people up.
mg again produced no output — looks like a stray keystroke or a no-op alias. No action taken; ready when you are.
Got it — mg is your git-viewer alias; it just can't render its TUI inside this session, so it comes back blank here. Nothing's broken on my end and I've not changed anything. Run it in a normal terminal tab instead. What would you like to do next?
wtf. first off, my terminal is plenty tall enough, i just shared a small screenshot. it's very weird. the repo picker is nice and tall, very usable, but the region picker seems clamped to 1-option-visible at any time which sucks. what can we do about this? Again, it's fine for the repo picker.
Fixed — 65d2deea0. And you're right; my earlier "short terminal" guess was wrong. Apologies for that.
Real cause
huh sizes an unset multi-select height to lipgloss.Height(options) − (title+description rows). So it's option-count-dependent, nothing to do with your terminal:
- Repo picker: many options → tall, fine.
- Region picker: 3 options →
3 − 2 ≈ 1visible row. That's the clamp.
Fix
Set .Height() explicitly on both pickers to option count + header slack, so the whole list renders (and still scrolls if it ever outgrows the terminal). The region picker now shows all clusters at once; the repo picker is unchanged-to-better (it'll now also show all without the ~2-row scroll it had, and it won't collapse if you ever have only a couple of repos).
Lint clean, tests pass. Branch now has 10 commits over #1518; not pushed.
ok progress!
Instead of "⣻ Creating 3 mirror(s)…" i want to see something inspired by Docker pull's progress:
one line each per CreateMirror call we fire off, independently showing status flicking from processing->ready, and once they're all ready, print the summary table you already have. this is looking sharp.
Done — 0dbc9eb23.
What you get now
The wizard's Creating N mirror(s)… spinner is replaced by a Docker-pull-style live block — one line per (repo, region):
Each line animates and flips creating → processing → ready (or failed/suspended/timed out) independently as that mirror's CreateMirror + clone poll advance. When all finish, your existing summary table + git clone block print below.
How it works
- A per-poll status callback now flows through
awaitMirrorReady→createAndAwaitMirror→createOneMirror, so each mirror reports its live status. - New
mirrorProgressrenderer repaints the block in place via ANSI (reusingspinnerFrames), guarded by a mutex with a 100ms ticker. - Labels use the full cluster host (
@ aws-us-east-2.entire.io) per your request. - One-shot
create <url>unchanged — passesniland keeps its single spinner. - Non-TTY (pipes/tests) degrades to one printed line per mirror as it finalizes — no cursor escapes (covered by
TestMirrorProgress_NonTTY).
Lint clean (0 issues), 6781 tests pass. Branch now has 11 commits over #1518; not pushed.
One caveat worth flagging: the in-place repaint assumes the block isn't scrolled by other output mid-run — standard for this kind of inline progress (same assumption Docker makes). Nothing else writes to stderr during the fan-out, so it's stable.
let's also add one newline headroom between these lines:
entirehq/cli-entire-metadata @ aws-eu-central-1 (eu) ✓ ready REPO REGION STATUS CLONE URL
(i.e. final "ready" line and the table header)
Done — 87592cf73. One blank line now separates the final progress lines from the table header. Lint clean, tests pass. Not pushed.
PR opened: https://github.com/entireio/cli/pull/1519
Summary
- Committed + pushed all 12 commits to
origin/cor-645-cli-smooth-mirror-onboarding-flow(lint clean before push, per the rule). - PR #1519 opened with base
chore/refresh-openapi-spec(#1518), since this branch is stacked on it — the diff shows only our work. Retarget tomainonce #1518 merges.
The brief description covers user impact (wizard, status-poll readiness, jurisdiction-aware regions, auth token) and what's unchanged (one-shot create, remove/collaborators).
there are some PR findings on https://github.com/entireio/cli/pull/1519 - address.
Done. Pushed 7a0ca5c55 and posted a summary comment on PR #1519.
Findings addressed (5 valid)
- Wizard not TTY-gated (Cursor) — added
interactive.CanPromptInteractively()check with a clear message pointing at the non-interactive form; both pickers now useform.RunWithContext(ctx)for clean Ctrl+C handling. - Empty mirrors skipped suspension check (Cursor) —
createAndAwaitMirrornow does oneGetMirrorfor an existing empty placement and surfacessuspended+ resume guidance (restores old behavior; fresh creates can't be suspended, so skipped). - Cancel reported as failures (Cursor) — a cancelled run now exits quietly instead of "N mirror(s) failed" +
context canceledspam. - Suspended/failed lacked detail (Copilot) — wizard now attaches the mirror id + resume command to those result errors.
auth tokenHTTPS (Copilot) — enforcesapi.RequireSecureURLunless--insecure-http-auth, matchingauth status.
Rejected (1 false positive)
- Loop-var capture (Copilot) — repo is Go 1.26; per-iteration loop vars (since 1.22) make the goroutine capture safe. The
copyloopvarlinter had already removed the explicit copy as redundant. Explained in the PR comment, no change.
Added TestRunMirrorCreateWizard_RequiresTTY; mise run check green (lint 0 issues, 6782 tests). Branch now 13 commits over #1518.
Several ways to force the non-interactive path in a real shell — pick whichever:
Any of these should print:
and exit non-zero (silently — no extra error dump).
Notes:
ENTIRE_TEST_TTY=0is the most reliable and self-documenting — it's the dedicated override and short-circuits before everything else (any non-empty value ≠1forces OFF;ENTIRE_TEST_TTY=1forces interactive back ON if you want to flip it).- Plain piping (
echo | entire …) won't do it — the probe checks/dev/tty, not stdin, so a pipe still finds the terminal. - Confirm the non-interactive arg form still works the normal way:
entire repo mirror create github.com/octocat/hello-world(uses the fixed default cluster, no wizard).
One caveat: you're running a binary on your PATH — make sure it's built from this branch (go build -o /tmp/entire ./cmd/entire && ENTIRE_TEST_TTY=0 /tmp/entire repo mirror create) rather than an older installed entire.
fix this one: https://github.com/entireio/cli/pull/1519#discussion_r3471836615
for this one https://github.com/entireio/cli/pull/1519#discussion_r3471836617 let's reword that - just say error, the mirror is suspended, contact support.
All done.
Round-2 review fixes (committed 0b20568fc, pushed)
- r3471836615 — "Wizard ignores context cancellation": both pickers already used
RunWithContext, but the gap washandleFormCancellationonly recognizinghuh.ErrUserAborted/ErrTimeout. huh wraps an externally-cancelled context ashuh: context canceled, which hit the error branch. FixedhandleFormCancellationto also treatcontext.Canceled/context.DeadlineExceededas a clean cancel. - r3471836617 — internal terminology: reworded the wizard's suspended message to
the mirror is suspended; contact support(and the failed one tothe initial clone failed; contact support) per your direction. Also reworded the one-shot'sexplainSuspendedMirrorthe same way — it had the identicalentire-core admin mirrors resume/ "core" leak — so both paths are consistent. Updated the two tests that asserted the old wording.
Lint clean, 6782 tests pass.
Copilot threads resolved
Replied to and resolved all three:
- loop-var capture — replied it's a false positive on Go 1.26 (per-iteration loop vars since 1.22;
copyloopvarremoved the explicit copy), resolved. - suspended/failed detail — noted we addressed it but landed on the "contact support" wording per the other reviewer, resolved.
auth tokenHTTPS — noted it's done (api.RequireSecureURL), resolved.
Branch now 14 commits over #1518.
One note for your awareness: I reworded the one-shot explainSuspendedMirror too (beyond the exact line you pointed at) for consistency — it surfaced the same internal admin command. Shout if you'd rather keep operator-facing detail there.
Done — e5692a019, pushed.
Fix (r3471873262 — "Poll aborts on transient errors")
awaitMirrorReady no longer fails the create on a single GetMirror glitch. It now retries on the poll interval and only gives up after 5 consecutive failures (resets on any success) or when the context ends. So:
- A brief network/API blip mid-clone → ride it out, keep waiting.
- A persistent error (deleted mirror, revoked auth) → still surfaces as
poll mirror status: …at the cap, not a spin-to-timeout. - ctx cancel/deadline → unchanged (clean cancel / timeout).
Added two subtests: transient-errors-then-ready (tolerated) and persistent-errors-give-up-at-cap (stops at 5 calls, doesn't reach the deadline).
Lint clean, 6784 tests pass. Branch now 15 commits over #1518.
I left the Cursor thread for its bot to re-mark on this push (you'd only asked me to resolve the Copilot threads). Say the word if you want me to resolve it manually too.
one final finding, on the Trail: https://entire.io/gh/entireio/cli/trails/660/smooth-mirror-onboarding-entire-repo-mirror-create-wizard
you can resolve it there when done.
hostFromPublicURL rejects any publicUrl whose parsed Path/RawQuery/Fragment is non-empty, so a catalog entry like https://aws-us-east-2.entire.io/ (trailing slash → Path == "/") is dropped via continue in clustersToRegions. If the control plane emits trailing slashes this silently removes clusters and can leave the wizard with "no regions available to mirror into". Consider tolerating a bare / path (or normalizing) since publicUrl is a trusted catalog field, not raw user input.
Confirmed: Open: 0, Resolved: 2. (The KeyError is just because unresolved_count drops out of the JSON once it hits zero — the counts block is authoritative.)
Final finding handled — trailing slash in publicUrl
Fixed hostFromPublicURL to tolerate a bare / path (https://aws-us-east-2.entire.io/), so a trailing-slash catalog entry is no longer silently dropped from clustersToRegions (which risked "no regions available"). Real paths/queries/fragments/userinfo are still rejected, since publicUrl flows into clone URLs and the STS audience. Committed 8fc7b1cdf, pushed. Added a trailing slash test case (and /sneaky still errors).
Trail #660 — both findings resolved
019efd04-bdcc…(trailing slash, low) → resolved, citing8fc7b1cdf.019efd04-bb7c…(transient poll errors, medium) → resolved, citinge5692a019(the earlier Cursor finding I'd already fixed — it was the same issue surfaced on the trail).
Trail now shows 0 open / 2 resolved. I resolved both since both were addressed in code; flag me if you wanted only the trailing-slash one closed.
Lint clean, 6785 tests pass. Branch is 16 commits over #1518, all pushed.