Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions fns_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

import yaml

# Default directories treated as config / settings sync
DEFAULT_CONFIG_SYNC_DIRS = [".obsidian", ".agents"]


@dataclass
class ServerConfig:
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 4 additions & 2 deletions fns_cli/file_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
23 changes: 23 additions & 0 deletions fns_cli/folder_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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():
Expand All @@ -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:
Expand Down
13 changes: 11 additions & 2 deletions fns_cli/setting_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
16 changes: 11 additions & 5 deletions fns_cli/sync_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
Loading