feat: headless CLI for vault-to-CRDT sync#16
Conversation
Two bugs prevented the filesystem watcher from working: 1. shouldIgnoreNormalizedPath treated null-stats paths as files. When chokidar calls _isIgnored for the root directory without stats, the path was checked against isMarkdownSyncable (returns false for directory names not ending in .md), causing the entire tree to be pruned. Fix: when stats are null, only ignore paths that are definitively non-markdown files (have an extension but not .md). 2. Chokidar's internal _isIgnored calls .map() on the ignored option. When ignored is a bare function, .map() throws TypeError (functions don't have .map), which is silently caught — the watcher starts but watches nothing. Wrapping in an array preserves the function through normalizeIgnored's type check. Also upgraded chokidar from 4.0.3 to 5.0.0.
P1: Add trailing slash removal to normalizeVaultPath to match Obsidian normalizePath() semantics, preventing CRDT key mismatches between plugin and headless clients. P1: Add requireRuntimeConfig() to status command so host/token/vaultId are validated before creating a sync client. P1: Move startMapObservers() before reconcileFromDisk() so remote edits arriving during startup disk scan are immediately mirrored. Safe because observers filter local origins (ORIGIN_SEED). P2: Resolve relative vault dir to absolute path in resolveCliConfig so YAOS_DIR/config values don't depend on process working directory. P2: Reject invalid externalEditPolicy values instead of silently defaulting to the most permissive policy (always). P2: Change createNodeVaultSync to accept RuntimeCliConfig instead of ResolvedCliConfig, making the type system enforce that connection settings are validated before sync client construction. Also includes prior uncommitted fixes: Promise.allSettled for batch writes, fs.rm cleanup on rename fallback, ENOENT handling during vault walk, early reconnection handler installation, reconcileInFlight flag, commander exitOverride, and prepare/dev scripts.
P1 fixes: - nodeDiskMirror: reject path traversal via .. segments in toAbsolutePath() - nodeDiskMirror: don't prune directories when chokidar has no stats - exclude: preserve trailing slash in user exclude patterns P2 fixes: - config: expand ~ in vault directory before resolve - cli: use strict regex for positive integer parsing - config: filter empty strings from CLI overrides in pickDefined - nodeVaultSync: gate reconnect callback until startup state initialized - nodeDiskMirror: write new file before deleting old in rename fallback
…resh # Conflicts: # package-lock.json
|
@kavinsood I refreshed this PR on top of current Mapping the #11 requirements / follow-up feedback to the current head:
Verification on the current head:
One remaining scope question for the VaultFS extraction PR: should path normalization, exclude semantics, and frontmatter-guard decisions live in the shared layer, or should those remain adapter/runtime responsibilities? My default will be conservative: extract filesystem primitives first and keep normalization/excludes/frontmatter behavior where it already lives unless you prefer otherwise. |
|
Review follow-up pushed in This addresses the review findings from the hardening pass:
Verification rerun on
|
|
Second-pass review found two remaining symlink-safety gaps in the rename fallback path; fixed in Changes:
Verification rerun on
|
fc810fe to
3837c05
Compare
Summary
Adds a headless Node.js CLI that mirrors a Markdown vault directory to a YAOS CRDT room without Obsidian. The supported alpha target is a Linux server using a local filesystem.
New:
packages/cli/yaos-cli daemon— startup reconciliation, filesystem watcher, graceful shutdown.yaos-cli sync— one reconciliation pass, then exit.yaos-cli status— connection/cache status as JSON.~/.config/yaos/cli.json.fsdisk mirror for Markdown files.VaultSyncCRDT engine with a Nodewspolyfill.Maintainer-feedback hardening
datasync, rename, parent-directory sync..yaos-state.binplus.yaos-state.json, loaded on startup for delta sync acrosssync/cron runs.yaos-cli syncwarns when no local state cache exists and the run must fetch room state before delta-syncing.update_requiredexits with status2so systemd can useRestartPreventExitStatus=2, including fatal auth received after daemon startup.Review follow-up hardening
./..segments are rejected before filesystem operations.Core/plugin changes
src/utils/normalizeVaultPath.tsis shared by plugin and CLI and now enforces NFC.src/sync/vaultSync.tsaccepts a persistence factory for non-browser runtimes, detects an already-synced provider before waiting for a future sync event, and exposes fatal-auth notification for headless daemon shutdown.Explicit constraints
Unsupported in this PR:
.obsidiansettings/plugin sync.Follow-up architecture
The VaultFS/core extraction is not bundled into this PR. I agree with the constraint that YAOS should not maintain two parallel monoliths; I can open a focused prerequisite PR for the VaultFS adapter boundary next, then rebase this CLI onto it if you prefer extraction-first sequencing.
Test plan
npm run @yaos/cli/typechecknpm run @yaos/cli/test— 17/17 passingnpm run @yaos/cli/buildnpm run buildgit diff --check