An experimental, simple, no-frills command-line WebDAV sync client written in plain JavaScript (Bun), with support for virtual folders.
LOCAL ROOT DAV ROOT
/my/files /remote/files
| |
+---- docs/report.pdf <== same path ==> docs/report.pdf
+---- photos/cat.jpg <== same path ==> photos/cat.jpg
| |
+---------------- SCAN BOTH ------------------+
fcks push = remote should look like local
fcks pull = local should look like remote
fcks merge = keep both sides; most recently modified file wins
fcks scaffold = copy remote folder structure, without files
fcks free = delete local files, but keep folders
fcks rm = delete a path locally and remotely
Features:
- No temporary files, no database
- Stateless
- Limited set of commands
- Virtual folder scaffolding
- Folder-tree mirroring
- File downloads and uploads, plus local folder cleanup, including subfolders
This project was born out of deep frustration with OneDrive and Nextcloud sync issues on macOS. Nevertheless, fcks supports all platforms.
Other popular solutions didn’t provide the kind of virtual-folder support I needed: a simple alternative to Files On-Demand. Instead of keeping placeholder files, fcks keeps only the folder structure visible and lets you "hydrate" its contents when needed.
fcks # help and configured DAV endpoint
fcks --config # local folder, DAV connection, and remote root
fcks --reset # delete the complete fcks app-data directory
fcks . # merge the current folder (same as `fcks merge .`)
fcks push [path] # make the remote subtree match local
fcks pull [path] # make the local subtree match remote
fcks merge [path] # copy both ways; most recently modified file wins; never delete
fcks scaffold [path] # recreate the remote folder structure locally
fcks free [path] # delete local files while retaining their folders
fcks rm <path> # delete a file/folder locally and remotely
fcks ls [path] # list shared, local-only, and remote-only entries
fcks push -f . # skip confirmation (`-f` works with every operation)
fcks pull -s . # select one remote child folder to pull
fcks push -s . # select one local child folder to pushShort aliases are available: ph for push, pl for pull, sc for
scaffold, and fr for free.
The path defaults to the current directory for commands with an optional path.
rm requires a path. push, pull, merge, free, and rm operate on an
exact file or folder target; scaffold and ls require a folder. Every target
must be the configured local root or a descendant; paths
outside it are rejected. Synchronization rejects symbolic links because
WebDAV cannot preserve their semantics safely. The relative path below
the configured local root is preserved below the configured DAV root. For
example, local root /foo and DAV root /bar map
/foo/docs/file.txt to /bar/docs/file.txt.
All mutating commands first display additions, changes, removals, and conflicts,
along with approximate totals to upload, download, or free locally or remotely,
then ask once for confirmation. -f auto-confirms; it goes before the optional
path, for example fcks pull -f ./archive or fcks -f ./archive for an
implicit merge.
pull -s [path] opens an interactive list of immediate remote child folders,
creates the selected folder locally, and pulls that subtree. push -s [path]
lists immediate local child folders and pushes the selected subtree. -s is
only available for pull and push, and cannot be combined with -f.
In an interactive terminal, regular sync commands use OpenTUI for the same
simple flow: scan, review the complete plan, confirm once, and watch progress.
ls also uses an OpenTUI browser for its combined local and remote listing.
When terminal interaction is unavailable, it falls back to a plain-text table
that can be redirected or piped.
Press Esc or Ctrl+C to interrupt planning or execution. No new work is
started, active hashes and transfers are aborted, and the result reports how
many operations had already completed. Those completed changes are kept; the
next push, pull, or merge reconciles any partial state. Downloads stream
directly into place without staging files, so interruption can leave a partial
destination file. Interrupted CLI runs exit with status 130.
-
lsis read-only and lists one folder level, like Unixls. -
pushandpullare intentionally authoritative and may delete files on the destination side. -
mergenever deletes; differing files are copied from the side with the later modification time. Equal-time content conflicts and file-versus-directory conflicts are reported and left unchanged. -
scaffoldcreates missing directories implied by remote files without modifying existing local files or folders. -
freeprints a merge-first safety warning. For a folder target, it removes all files below it without removing directories; for a file target it removes only that file. -
rmchecks the exact path on both sides and removes whichever copies exist. Remote directory deletion includes its complete subtree. A missing local copy does not prevent the remote copy from being found and removed.
Hidden files and folders are included. Empty folders are ignored, like Git.
For push and merge, a .fcksignore file can exclude files and folders
below the directory that contains it. Its syntax follows Git ignore patterns:
blank lines and # comments are skipped, glob patterns are supported, a
trailing / matches folders, and ! negates a previous pattern. Nested
.fcksignore files apply to their own subtrees. If the same ignore file exists
locally and remotely, the newer copy supplies the rules for that operation.
The .fcksignore files themselves are synced normally, and free removes them
along with all other local files.
The implementation keeps no sync database. It uses bounded-concurrency scans, streaming hashes, direct streamed downloads, and streamed uploads, so memory use does not scale with file size. WebDAV ETags are treated as opaque validators. Equal-sized files at the same path are streamed through SHA-256 on both sides for an accurate comparison.
Note that standard WebDAV does not carry portable POSIX ownership, ACL, executable-bit, or extended-attribute metadata, so those attributes are not synchronized between machines.
fcks --reset recursively removes the entire OS-specific fcks configuration
directory.
Publishing a GitHub Release builds standalone fcks executables for x64 and
ARM64 versions of Linux, Windows, and macOS. These executables include Bun, so
users do not need a separate Bun installation.
To make the executable available system-wide, rename the binary to fcks
(fcks.exe on Windows) and move it to:
- Windows:
C:\Program Files\fcks\and add this directory to the systemPATH - GNU/Linux:
/usr/local/bin/ - macOS:
/usr/local/bin/
On GNU/Linux and macOS, make the file executable:
chmod 755 /usr/local/bin/fcksNote: On macOS, you might need to approve the first run in the security settings because the app is not signed.
If you already have Bun installed, you can run the project directly:
- Clone this repository and
cdinto it. - Run these commands to temporarily add
fcksto yourPATH:
bun install
bun linkbun install
bun run src/cli.js --configThe compact configuration flow first asks for a local folder and then for DAV credentials. It verifies the connection and lets you browse, create, and choose a remote root. The configuration is written with owner-only permissions to the native user configuration directory:
- macOS:
~/Library/Application Support/fcks/config.json - Linux:
$XDG_CONFIG_HOME/fcks/config.jsonor~/.config/fcks/config.json - Windows:
%APPDATA%\\fcks\\config.json
Warning: FCKS stores the password in the "protected" JSON configuration file.
Important: All remote paths are relative to the configured DAV root.
Traversal with .. (leading to the folder outside the configured root) is rejected.
Happy FCKSing, everyone!
