# feat(mirror): split placement and clone into distinct progress steps

`88aa100`→[main](/content/gh/entireio/cli/commits/main/index.html)·

toothbrush·2w ago·2 files·+40 added/-11 removed

\`repo mirror create <url> <cluster>\` now shows a "Placing mirror" spinner
for the fast CreateMirror call, then a separate "Cloning" spinner for the
readiness poll. Already-ready mirrors skip the Cloning line entirely.

Also: "Mirror exists" (was "already exists") and a leading newline before
the mirror summary for breathing room.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

## Sessions

e4b1b656c328View transcript

## Changes

2

- cmd/entire/cli

- Mrepo\_mirror.go+39/-10
  
  - Mrepo\_mirror\_create\_wizard.go+1/-1

```
171 unmodified lines

172
173
174
175
176
175
176
177
178
179
180
181
182
183
184
185
178
179
180
181
182
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
20 unmodified lines

228
229
230
209
231
232
233
234
235
236
237
238
3 unmodified lines

242
243
244
245
246
247
248
249
250
32 unmodified lines

283
284
285
257
286
287
259
288
289
290
291

171 unmodified lines

}
return runCoreForCluster(cmd, clusterHost, func(ctx context.Context, c *coreapi.Client) error {
errW := cmd.ErrOrStderr();
stop := startSpinner(errW, fmt.Sprintf("Cloning %s/%s into %s", owner, repo, clusterHost));
// nil onStatus: the one-shot's single spinner shows liveness; the
// Two-phase progress: a "Placing" spinner covers the fast
// CreateMirror call (placement, <15s), then a separate "Cloning"
// spinner covers the clone-readiness poll. An already-ready mirror
// completes the first poll faster than the spinner's initial delay,
// so the Cloning line never paints and we go straight to the clone
// instructions.
placing := startSpinner(errW, fmt.Sprintf("Placing mirror %s/%s into %s", owner, repo, clusterHost));
placed := false;
var cloning func(success bool);
// nil onStatus: the one-shot's spinners show liveness; the
// per-mirror progress lines are the wizard's concern.
outcome, err := createAndAwaitMirror(ctx, c, owner, repo, clusterHost, noWait, waitTimeout, nil);
// Only a confirmed-ready clone earns the ✓; everything else
// (empty, --no-wait, suspended, failed, timeout) erases the line
// and lets reportOneShotMirror print the specific outcome.
stop(err == nil && outcome.polled && outcome.status == coreapi.MirrorStatusReady);
outcome, err := createAndAwaitMirror(ctx, c, owner, repo, clusterHost, noWait, waitTimeout,
func(created *coreapi.CreatedMirror) {
placing(true);
placed = true;
// Only start a Cloning spinner when there's a clone to await.
if !noWait && !created.Empty {
cloning = startSpinner(errW, fmt.Sprintf("Cloning %s/%s into %s", owner, repo, clusterHost));
}
}, nil);
if !placed {
// CreateMirror failed before onCreated fired — erase the line.
placing(false);
}
if cloning != nil {
// Only a confirmed-ready clone earns the ✓; everything else
// (suspended, failed, timeout) erases the line and lets
// reportOneShotMirror print the specific outcome.
cloning(err == nil && outcome.polled && outcome.status == coreapi.MirrorStatusReady);
}
return reportOneShotMirror(cmd.OutOrStdout(), errW, outcome, err);
});
},
20 unmodified lines

// status. The returned error is the create error (when outcome.created is nil)
// or the wait error — a status sentinel (errMirrorCloneFailed /
// errMirrorSuspended) or a timeout; callers read outcome.status for the state.
func createAndAwaitMirror(ctx context.Context, c *coreapi.Client, owner, repo, clusterHost string, noWait bool, timeout time.Duration, onStatus func(coreapi.MirrorStatus)) (mirrorCreateOutcome, error) {
//
// onCreated (may be nil) fires once the placement is registered, before any
// clone polling — it separates the fast "placing" phase from the slow "cloning"
// wait so callers can render them as distinct steps.
func createAndAwaitMirror(ctx context.Context, c *coreapi.Client, owner, repo, clusterHost string, noWait bool, timeout time.Duration, onCreated func(*coreapi.CreatedMirror), onStatus func(coreapi.MirrorStatus)) (mirrorCreateOutcome, error) {
created, err := c.CreateMirror(ctx, &coreapi.CreateMirrorInputBody{
Provider: coreapi.CreateMirrorInputBodyProviderGithub,
Owner: owner,
3 unmodified lines

if err != nil {
return mirrorCreateOutcome{}, err;
}
if onCreated != nil {
onCreated(created);
}
outcome := mirrorCreateOutcome{created: created};
if created.Empty {
// An empty upstream has nothing to clone, so don't poll for "ready" — it
32 unmodified lines

return err;
}
if created.Created {
fmt.Fprintf(out, "Registered mirror %s\n", created.MirrorId);
fmt.Fprintf(out, "\nRegistered mirror %s\n", created.MirrorId);
} else {
fmt.Fprintf(out, "Mirror already exists (%s)\n", created.MirrorId);
fmt.Fprintf(out, "\nMirror exists (%s)\n", created.MirrorId);
}
fmt.Fprintf(out, "  %s\n", created.MirrorUrl);
```

Mcmd/entire/cli/repo\_mirror.go+39/-10

```
505 unmodified lines

506
507
508
509
509
510
511
512

505 unmodified lines

// Same create-then-wait path as the one-shot `repo mirror create <url>`
// (createAndAwaitMirror), so both report identical lifecycle states. The
// per-poll status drives this mirror's progress line.
outcome, err := createAndAwaitMirror(ctx, c, t.owner, t.repo, t.region.host, noWait, waitTimeout,
outcome, err := createAndAwaitMirror(ctx, c, t.owner, t.repo, t.region.host, noWait, waitTimeout, nil,
func(s coreapi.MirrorStatus) { report(string(s), false, false) });
if outcome.created == nil {
res.status, res.err = mirrorStatusError, renderCoreError(err);
