Migrate iCloud Desktop Assets
Procedure
- Read the active
AGENTS.mdand placement policy. Resolve every source and destination to explicit absolute paths. - Confirm each source is a real directory and every destination is absent. Capture per-child-directory file and media counts before moving.
- For each pair, run
mv <source> <destination> && ln -s <destination> <source>in one command list. Do not usecporrsyncfor same-volume moves. - Update every producer output root, its docstring, all consumer roots and account configuration, and current specs or docs.
- Back up each mutable ledger as
.bak, then mechanically replace the old absolute-path prefix. Include all related media pools whose names share that prefix. - Search ignored and tracked JSON, Python, and JavaScript files. Exclude logs and
.bak, but do not rely on defaultrgignore behavior. - Validate JSON, test the real consumer path, force a full PIL image load from the destination, and compare the post-move counts with the captured pre-move counts.
- Recheck Desktop after iCloud has had time to react. Inspect any conflict directory before removing it, and use
rmdironly when it is empty. - Commit only task-owned tracked files. Keep ledger backups local and preserve unrelated working-tree changes.
Pitfalls
- Default
rgskips gitignored ledgers and state. Userg --no-ignoreor the user's exact recursivegrepcommand for the final zero-match proof. statusis read-only in zsh. Use a name such asgrep_codewhen recording pipeline exits.- iCloud may recreate an old folder as
<name> 2after a symlink appears. Treat it as unknown until a direct content and metadata check proves it empty. - Do not fabricate a missing date directory to satisfy a range-shaped expectation. Preserve the pre-move state and report the pre-existing gap.
- A recursive
grepover a large development tree can take minutes. Poll the running process instead of treating empty intermediate output as completion. - An untracked
.bakis intentional recovery state, not a file to stage automatically.
Verification
ls -ld <legacy-paths>shows only symlinks pointing to the new root.- Every pre-existing child directory has the same file count before and after; aggregate media totals also match.
- The consumer returns absolute paths under the new root, and its duplicate filter recognizes rewritten ledger entries.
- PIL
Image.open(path); image.load()succeeds on a real destination image. - The final old-prefix search returns zero non-log, non-backup JSON, Python, and JavaScript matches.
jq emptypasses for all edited JSON, andcmpconfirms each ledger equals its backup with only the prefix replacement applied.- Targeted syntax checks and project tests pass. If code is shipped, local and remote commit hashes match afterward.