Skip to content

feat: add prune command to remove dangling overlay links - #6

Merged
lollipop-onl merged 1 commit into
mainfrom
feat/prune
Jul 17, 2026
Merged

feat: add prune command to remove dangling overlay links#6
lollipop-onl merged 1 commit into
mainfrom
feat/prune

Conversation

@lollipop-onl

Copy link
Copy Markdown
Member

Problem

apply only ever creates or repoints symlinks — it never removes one.
When a file is deleted from an overlay entry after apply has already
linked it into one or more checkouts, the symlink it created becomes a
dangling ghostq-managed link: it still points into this repo's
overlay entry, but the target no longer exists. Nothing in the CLI
could clean these up.

Approach

pruneFiles (in src/apply.ts, following the "modules return data,
index.ts prints" convention) walks the checkout tree directly,
skipping .git, rather than the overlay entry — collectEntryFiles
can't help here since the overlay file behind a dangling link is, by
definition, already gone. For every symlink it finds, it checks two
things:

  1. does the target resolve inside this repo's overlay entry dir
    (resolve(target).startsWith(resolve(ctx.entryDir) + "/")) — i.e.
    is it ghostq-managed?
  2. is the target actually missing on disk — i.e. is it dangling?

Only links satisfying both are removed, and only after an additional
git check-ignore gate (mirroring apply's own gating) confirms the
path isn't committed. After removing a dangling link, prune
opportunistically walks up removing now-empty parent directories, but
stops as soon as it hits a directory that still has other entries, and
never goes above the repo root.

Wired up as ghostq prune [path], added to USAGE in src/index.ts
and to the usage block / a new safety note in README.md.

Safety guarantees

  • A link whose target still exists (a live overlay link) is never
    touched — that's apply's job, not prune's.
  • Non-symlink files and symlinks pointing outside the overlay entry
    (user data) are never touched.
  • A directory is only removed if it became empty as a direct result of
    pruning; directories with other entries are left alone.
  • Idempotent: a second run after pruning is a no-op.

Test plan

  • bun test — 42 pass, including a new test/prune.test.ts file
    covering: dangling link removal, live links left untouched, real
    user files / symlinks-to-outside left untouched, empty-directory
    cleanup vs. non-empty directories, and idempotency.
  • bun run typecheck — clean.
  • Manually exercised ghostq apply → delete overlay file → ghostq prune → re-run prune against a scratch repo; confirmed the
    dangling link is removed and the second run is a no-op.

🤖 Generated with Claude Code

https://claude.ai/code/session_013mAzjNvVWGBJT81cfTSJJN

apply only ever creates or repoints symlinks; it never removes one.
Deleting a file from an overlay entry after apply has already linked
it leaves a dangling ghostq-managed symlink behind in every checkout
that linked it — nothing in the CLI could clean that up.

collectEntryFiles can't help here since it walks the overlay entry,
and the whole point of a dangling link is that its overlay file is
gone. prune instead walks the checkout tree directly (skipping .git)
and, for each symlink found, checks whether it resolves inside this
repo's overlay entry (proving it's ghostq's) but no longer exists on
disk (proving it's dangling) — mirroring apply's no-clobber and
check-ignore invariants so live links and user files are never
touched. Empty parent directories left behind are removed too, but
only ones that became empty as a result, never a directory that still
holds other entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013mAzjNvVWGBJT81cfTSJJN
@lollipop-onl
lollipop-onl merged commit a3f961e into main Jul 17, 2026
1 check passed
@lollipop-onl
lollipop-onl deleted the feat/prune branch July 17, 2026 14:56
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.

1 participant