Skip to content

feat(sync): restore missing files and archive only with detach --archive - #857

Merged
ctawiah merged 5 commits into
mainfrom
ctawiah/sync-restore-missing-files
Oct 14, 2026
Merged

ctawiah merged 5 commits into
mainfrom
ctawiah/sync-restore-missing-files

Conversation

@ctawiah

@ctawiah ctawiah commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Context

A missing local file made sync archive the variation in LaunchDarkly. A file is often missing only because the working copy does not have it yet, for example after a Git pull or a branch switch. A plain sync, or a watch session that applies changes, could then archive a variation that other engineers use.

This PR is stacked on #856. That PR makes sync.lock track every project, so sync can find a variation whose file is missing.

What changes

  • Sync restores a missing local file from LaunchDarkly. The sync plan never archives a variation, so the archive_server action is removed.
  • ldcli sync prompts detach <selector> --archive is the only way to archive a variation. It archives each selected variation in LaunchDarkly, and then stops syncing it.
  • detach --archive asks for confirmation in a terminal. With --no-input, it needs --yes, the same rule as other destructive changes.
  • The archive runs first, and an already archived variation does not fail. If a later step fails, the same command can run again.
  • The confirmation prompt moves to the interactive package, so sync and detach use one copy.
  • The command is now ldcli sync prompts, plural, like the other resource commands. There is no alias for sync prompt.

Stop syncing a variation and archive it:

ldcli sync prompts detach production/support/default --archive --yes --no-input

Review focus

  • Is it right that a missing file always comes back, and that only detach --archive archives?
  • Is the order in detach --archive safe when a step fails: archive, then baseline, then file deletion?
  • Is the archive confirmation clear in the terminal and with --no-input?

Verification

  • go build ./...
  • go vet ./...
  • go test -race ./internal/sync/... ./cmd/sync/...
  • TestPromptRestoresTrackedMissingFileInsteadOfArchiving deletes a tracked file, runs sync, and checks that the file comes back and nothing is archived.

Related changes

  1. feat(sync): keep each working copy's baseline in sync.lock #856: keep each working copy's baseline in sync.lock
  2. This PR: restore missing files and archive only with detach --archive

Devin Review


Note

Overview
Sync no longer archives missing local variations. A missing managed file (e.g. after git pull or branch switch) is treated as restore from LaunchDarkly instead of pushing an archive. The archive_server plan action and related conflict/diff paths are removed; server-side archive is only via detach.

ldcli sync prompts detach --archive optionally archives selected variations in LaunchDarkly before stopping sync. Archive runs first (idempotent if already archived), then baseline and local cleanup; terminal confirmation or --yes with --no-input. Shared interactive.Confirm powers sync apply and detach archive prompts; conflict resolution uses SelectInline so prior output stays visible.

CLI rename: sync prompt → sync prompts (examples/tests updated; no alias).

Baseline / sync.lock: Copies without a lock file no longer inherit the remote manifest as their baseline (avoids tracking other branches’ entries). version is not written to sync.lock (versions stay on the remote manifest for optimistic updates). ref for linked variations is stored in the lock and used when recreating missing wrapper files; restore fails safely if the linked file is missing or diverges.

Breaking: Scripts using ldcli sync prompt must switch to sync prompts. Behavior change: deleting a tracked file and running sync restores it rather than archiving in LD.

Reviewed by Cursor Bugbot for commit 3d1afe5. Bugbot is set up for automated code reviews on this repo. Configure here.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Devin Review

Comment thread internal/sync/prompt/plan.go Resolved
Comment thread internal/sync/detach/detach.go Resolved
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Review notes. I made no code changes.

1. Medium: you cannot safely rerun detach --archive, because archiving an archived variation returns 400, not 404

archiveVariations (internal/sync/detach/detach.go) skips only syncapi.IsNotFound errors, and IsNotFound matches only HTTP 404. The comment says "a variation that is already archived does not fail". But in gonfalon, Variation.Archive returns domain.NewError("cannot archive an archived variation"), and error_response.go maps domain.Error to ferror.NewBadRequest (400). The unit test case "variation already archived" in detach_test.go fakes a 404, so it does not match the real API.

Effect: archive runs before detachResources. If the baseline save or the file delete fails after the archive succeeds, the variation is still tracked. Then every rerun of the same command fails with archive variation ...: cannot archive an archived variation. With more than one selection, the loop stops at the first error. The earlier variations are then archived but still tracked, and a rerun fails on them. The user has to know to run plain detach (or sync) to recover.

2. Low: --archive deletes local edits that were never pushed, without warning

The confirm prompt only asks Archive these variations in LaunchDarkly? [y/N]. Then detachResources deletes the local files even if they have changes that were never pushed. After that, the content survives only in Git history, or in the archived variation if the edits had been pushed. You could say this in the prompt, or check for unpushed local changes first.

3. Inherited from #856: first sync with no sync.lock restores other branches' prompts

Because the baseline falls back to the shared remote manifest when there is no lock, the new "missing file means restore" rule pulls variations from other branches into this branch (checked with a scratch test: second action=update_local). This is no longer data loss as it was in #856, but it is likely not what users expect. See the #856 comment.

Written by Devin

Comment thread internal/sync/detach/detach.go Resolved
Comment thread internal/sync/prompt/plan.go Resolved
Comment thread cmd/sync/prompt.go Resolved
Comment thread internal/sync/detach/detach_test.go Resolved

@tonytrinh3 tonytrinh3 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ctawiah Overall it looks good to me. Devin pointed out additional tests that needed to be added.

@ctawiah
ctawiah requested a review from tonytrinh3 October 8, 2026 23:32
@ctawiah
ctawiah marked this pull request as ready for review October 9, 2026 20:04
@ctawiah
ctawiah requested a review from a team as a code owner October 9, 2026 20:04

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4b72020. Configure here.

Comment thread internal/sync/manifest/model.go Resolved
@tonytrinh3

Copy link
Copy Markdown

https://app.devin.ai/sessions/ebd00294540f4fc09e7d8ed7905c2e84?tab=browser

Base automatically changed from ctawiah/sync-lock-baseline to main October 14, 2026 21:11
A missing local file made sync archive the variation in LaunchDarkly.
A file is often missing only because the working copy does not have it
yet, for example after a Git pull or a branch switch. A plain sync could
then archive a variation that other engineers use.

Sync now restores a missing file from LaunchDarkly. The sync plan never
archives a variation. "ldcli sync prompt detach --archive" is the only
way to archive one. It archives each selected variation, and then stops
syncing it. It asks for confirmation, and with --no-input it needs
--yes. The archive runs first, and an archived variation does not fail,
so the same command can run again after a later step fails.

The confirmation prompt moves to the interactive package, so that sync
and detach use the same prompt.
A restored variation file lost its link to the prompt file that it used.
The lock now records the link of each variation, and a restore keeps it.
Sync stops with a clear error, and writes nothing, when it cannot restore
the link safely. This happens when the working copy has no sync.lock,
when the linked file is not available, or when the linked file has local
edits.

Detach now reads each variation before it archives it, because the API
rejects the archive of an archived variation. A rerun after a failed step
skips the variations that are already archived and finishes. Detach also
uses the command context, so a canceled command does not archive.

The lock no longer stores the remote version of each entry. Each save
still sends the version that sync read, so optimistic locking does not
change. A lock entry now changes only when its content or its link
changes, which keeps Git diffs and merge conflicts small.

New tests cover linked restores, unsafe restores, a partial failure with
several variations, a failed delete followed by a rerun, the interactive
archive confirmation, and detach --archive through the CLI.
Every other ldcli resource command uses a plural noun, such as flags,
projects, and segments. The command also syncs every prompt variation
in the workspace, not one. "ldcli sync prompts" now replaces
"ldcli sync prompt", with no alias.
The remote manifest is shared by every branch. A working copy without
sync.lock used it as its baseline, so variations from other branches
looked tracked and missing. Sync then restored or archived them.

Now a missing sync.lock gives an empty baseline. The first sync adopts
the local files and writes the lock. This removes the HasLockFile flag,
the lock write on a sync without changes, and the restore error for a
working copy without sync.lock.
The conflict choice opened in the terminal's alternate screen. That
screen hid the diff that sync writes just before the question, so the
user saw only the three choices.

The conflict choice now shows below the current output, and the diff
stays above it. The project, config, and model config pickers keep the
full screen, because their lists can be long.
@ctawiah
ctawiah force-pushed the ctawiah/sync-restore-missing-files branch from fb0cf44 to 3d1afe5 Compare October 14, 2026 21:11
@ctawiah
ctawiah merged commit d755910 into main Oct 14, 2026
10 checks passed
@ctawiah
ctawiah deleted the ctawiah/sync-restore-missing-files branch October 14, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants