Skip to content

✨ feat(sync): inspect and repair the local pCloud sync daemon - #1

Merged
kud merged 2 commits into
mainfrom
feat/local-sync-inspection
Jul 31, 2026
Merged

kud merged 2 commits into
mainfrom
feat/local-sync-inspection

Conversation

@kud

@kud kud commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Why

Every command in this CLI talks to the pCloud API. None of them can see the
local database the pCloud Drive desktop app keeps at ~/.pcloud/data.db,
which is where sync pairs actually live — so a broken sync pair is invisible to
pcloud doctor and to every other command here.

This was found the hard way. A sync pair had lost its remote folder:
syncfolder.folderid is declared ON DELETE SET NULL, so when the remote folder
was deleted, SQLite blanked the reference rather than removing the pair. The pair
survived as a zombie pointing at nothing, alongside a healthy duplicate for the
same local folder. pCloud Drive rendered the null remote as / and reported the
failure as:

pCloud doesn't have permissions to upload this item.

Which is neither a permissions problem nor a folder named /. Diagnosing it took
manual SQLite spelunking; this PR makes it one command.

What

Command Purpose
pcloud sync Table of sync pairs — local path, remote path, file count, queue depth, health
pcloud sync <id> Detail for a single pair, including stranded queue entries
pcloud sync --json Machine-readable output
pcloud sync --debug Daemon state, database size, WAL status, per-table row counts
pcloud sync prune <id> Remove an orphaned pair — dry run by default
pcloud doctor Now runs the same checks as a second section, after credential reach

Five checks, each derived from a real failure mode in the schema:

  • orphanedsyncfolder.folderid IS NULL; the fault above
  • remote-missingfolderid no longer present in the folder table
  • duplicate — two pairs claiming the same local path
  • local-missing — the local folder is gone from disk
  • stucktask rows with itemid = 0, i.e. queued with nowhere to go

Notes for review

Foreign keys are explicitly disabled. node:sqlite enables them by default
where pCloud's own writer does not. Enforcing constraints we did not author would
reject the broken rows these checks exist to find — a dangling folderid is the
bug, so a connection that refuses to read one is useless. See the comment on
OPEN in src/lib/sync.ts.

All reads go through a snapshot. The daemon holds the database under an
exclusive WAL lock while it runs, so opening it in place fails outright rather
than degrading to a stale read. snapshot() copies data.db plus its -wal and
-shm to a temp dir and opens the copy; the WAL files are absent after a clean
checkpoint, which is treated as valid rather than as a broken database.

The debug view uses an allowlist, never an exclusion list. The same database
holds setting, cryptofilekey and cryptofolderkey — auth token and crypto
key material. A deny-list would leak the first sensitive table pCloud adds in a
future release. Withheld tables are named, never read. There is a test asserting
all three are absent from DEBUG_TABLES.

prune --apply is gated three ways — dry run by default, backs up data.db
before writing, and refuses to run while pCloud Drive is up (checked via both
process list and lsof, since the process may have exited while still holding
the file). It deletes child-first inside a transaction and touches no files on
disk or in the cloud.

doctor's local section runs unauthenticated. It needs no credential, and a
broken sync pair is exactly the fault someone hits before getting round to
logging in — so only the remote half is given up on.

Testing

npm test — 17 tests, all passing. npm run typecheck clean.

The tests build their fixture from CREATE TABLE rather than snapshotting a real
database. That is deliberate: copying a live WAL mid-write yields a torn log whose
recovered contents differ between opens, so a scraped fixture produces failures
that mean nothing. Vitest is added as the test runner — this repo previously had
none.

Verified manually against a real database containing the orphan, exercising every
surface: the table, single-pair detail, --json, --debug, the prune dry run,
and the running-daemon guard (refused correctly, exit 1).

Follow-ups

  • The engine is CLI-local in src/lib/sync.ts, mirroring how src/lib/health.ts
    sits beside doctor. If an MCP or Raycast surface ever wants local sync state,
    it should move into @kud/pcloud rather than being duplicated.
  • pcloud doctor remains undocumented in the README and docs/ — pre-existing
    drift, deliberately left alone here.
  • macOS only: daemon detection uses pgrep/lsof against the .app bundle path.

kud added 2 commits July 31, 2026 14:14
- Add `sync status` to inspect local sync pairs and report health issues
- Add `sync prune` to remove orphaned sync pairs and their local indices
- Implement database snapshot reading that bypasses WAL locks
- Add comprehensive tests and documentation for sync operations
- Detect orphaned, remote-missing, and stuck sync issues with visual indicators
- Add `npm test` step to `.github/workflows/ci.yml`, between typecheck and build
- Ensures test failures block CI before a build is attempted, closing a gap where only typecheck and build were gated
@kud
kud marked this pull request as ready for review July 31, 2026 13:45
@kud
kud merged commit 4e9e515 into main Jul 31, 2026
1 check passed
@kud
kud deleted the feat/local-sync-inspection branch July 31, 2026 13:45
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