diff --git a/fns_cli/config.py b/fns_cli/config.py index 182f47b..13baa04 100644 --- a/fns_cli/config.py +++ b/fns_cli/config.py @@ -6,6 +6,9 @@ import yaml +# Default directories treated as config / settings sync +DEFAULT_CONFIG_SYNC_DIRS = [".obsidian", ".agents"] + @dataclass class ServerConfig: @@ -25,6 +28,9 @@ class SyncConfig: default_factory=lambda: [".git/**", ".trash/**", "*.tmp"] ) file_chunk_size: int = 524288 + config_sync_dirs: list[str] = field( + default_factory=lambda: list(DEFAULT_CONFIG_SYNC_DIRS) + ) @dataclass @@ -91,6 +97,9 @@ def load_config(path: str) -> AppConfig: "exclude_patterns", [".git/**", ".trash/**", "*.tmp"] ), file_chunk_size=s.get("file_chunk_size", 524288), + config_sync_dirs=s.get( + "config_sync_dirs", list(DEFAULT_CONFIG_SYNC_DIRS) + ), ) if "client" in raw: diff --git a/fns_cli/file_sync.py b/fns_cli/file_sync.py index f152f04..bd333b5 100644 --- a/fns_cli/file_sync.py +++ b/fns_cli/file_sync.py @@ -501,9 +501,11 @@ def _collect_local_files(self) -> list[dict]: if self.engine.is_excluded(rel) or rel.endswith(".md"): continue first = rel.split("/")[0] - if first.startswith(".") and not self.config.sync.sync_config: + # Skip dot-prefixed directories that are handled by SettingSync + # (.obsidian, .agents, and other config dirs) + if first.startswith(".") and self.config.sync.sync_config: continue - if not first.startswith(".") and not self.config.sync.sync_files: + if not self.config.sync.sync_files: continue try: stat = fp.stat() diff --git a/fns_cli/folder_sync.py b/fns_cli/folder_sync.py index 838267a..0945d6f 100644 --- a/fns_cli/folder_sync.py +++ b/fns_cli/folder_sync.py @@ -39,12 +39,27 @@ def register_handlers(self) -> None: ws.on(ACTION_FOLDER_SYNC_DELETE, self._on_sync_delete) ws.on(ACTION_FOLDER_SYNC_RENAME, self._on_sync_rename) + def _is_config_dir(self, rel_path: str) -> bool: + """Check if a path is in a config directory managed by SettingSync.""" + first = rel_path.split("/")[0] + if not first.startswith("."): + return False + # Use the same logic as SyncEngine._is_config() + config = self.engine.config.sync + if first in config.config_sync_dirs: + return True + return config.sync_config + async def _on_sync_modify(self, msg: WSMessage) -> None: data = _extract_inner(msg.data) rel_path: str = data.get("path", "") if not rel_path: return + if self._is_config_dir(rel_path): + log.debug("Ignoring FolderSyncModify for config dir: %s", rel_path) + return + full = self.vault_path / rel_path try: full.mkdir(parents=True, exist_ok=True) @@ -58,6 +73,10 @@ async def _on_sync_delete(self, msg: WSMessage) -> None: if not rel_path: return + if self._is_config_dir(rel_path): + log.debug("Ignoring FolderSyncDelete for config dir: %s", rel_path) + return + full = self.vault_path / rel_path try: if full.exists(): @@ -73,6 +92,10 @@ async def _on_sync_rename(self, msg: WSMessage) -> None: if not old_path or not new_path: return + if self._is_config_dir(old_path) or self._is_config_dir(new_path): + log.debug("Ignoring FolderSyncRename for config dir: %s → %s", old_path, new_path) + return + old_full = self.vault_path / old_path new_full = self.vault_path / new_path try: diff --git a/fns_cli/setting_sync.py b/fns_cli/setting_sync.py index 2c06101..6dc26b8 100644 --- a/fns_cli/setting_sync.py +++ b/fns_cli/setting_sync.py @@ -39,15 +39,24 @@ def _extract_inner(msg_data: dict) -> dict: return msg_data if isinstance(msg_data, dict) else {} -def _is_config_path(rel: str) -> bool: +def _is_config_path(rel: str, config_sync_dirs: list[str] | None = None) -> bool: """Check whether a relative path belongs to config/settings scope. This matches the Obsidian plugin behaviour: anything inside a dot-prefixed directory (e.g. .obsidian, .agents) is treated as a setting file. Standard exclusions (.git, .trash) are handled by is_excluded() upstream. + + config_sync_dirs: configured dot-prefixed directories to treat as config + (from config.yaml, e.g. [".obsidian", ".agents"]) """ first = rel.split("/")[0] - return first.startswith(".") + if not first.startswith("."): + return False + # Check configured config sync directories + if config_sync_dirs and first in config_sync_dirs: + return True + # By default, treat all dot-prefixed dirs as config (backward compatible) + return True class SettingSync: diff --git a/fns_cli/sync_engine.py b/fns_cli/sync_engine.py index 137af03..c27a95a 100644 --- a/fns_cli/sync_engine.py +++ b/fns_cli/sync_engine.py @@ -53,7 +53,13 @@ def _is_note(self, rel_path: str) -> bool: def _is_config(self, rel_path: str) -> bool: first = rel_path.split("/")[0] - return first.startswith(".") + if not first.startswith("."): + return False + # Check if the directory is in the configured config_sync_dirs list + if first in self.config.sync.config_sync_dirs: + return True + # For other dot-prefixed dirs, check if sync_config is enabled + return self.config.sync.sync_config def _should_sync_file(self, rel_path: str) -> bool: if self._is_config(rel_path): @@ -230,7 +236,7 @@ async def _initial_sync(self) -> None: await self.note_sync.request_sync() await self._wait_note_sync(timeout=300) - if self.config.sync.sync_files or self.config.sync.sync_config: + if self.config.sync.sync_files: await self.file_sync.request_sync() await self._wait_file_sync(timeout=300) @@ -284,16 +290,16 @@ async def _wait_setting_sync(self, timeout: float = 60) -> None: await asyncio.sleep(0.5) async def _push_all_files(self) -> None: - """Upload every non-note, non-excluded file in the vault.""" + """Upload every non-note, non-excluded, non-config file in the vault.""" for fp in self.vault_path.rglob("*"): if fp.is_dir(): continue rel = fp.relative_to(self.vault_path).as_posix() if self.is_excluded(rel) or rel.endswith(".md"): continue - if not self._is_config(rel) and not self.config.sync.sync_files: + if self._is_config(rel): continue - if self._is_config(rel) and not self.config.sync.sync_config: + if not self.config.sync.sync_files: continue await self.file_sync.push_upload(rel) await asyncio.sleep(0.05)