Last active
April 12, 2026 14:33
-
-
Save reagle/e6da63b553662bd1554dabc5ed6661b8 to your computer and use it in GitHub Desktop.
Create symbolic links from source Obsidian vault config to target vaults.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S uv run | |
| # /// script | |
| # dependencies = [] | |
| # /// | |
| """Sync shared Obsidian configuration from main vault to other vaults. | |
| Copies appearance, plugins, and settings while preserving vault-specific | |
| workspace and cache files. | |
| """ | |
| import argparse | |
| from pathlib import Path | |
| from shutil import copy2, copytree, rmtree | |
| def sync_config(main_vault: Path, other_vault: Path, dry_run: bool = False) -> None: | |
| """Sync shared config files from main vault to another vault. | |
| Copies files/directories that should be shared across vaults while | |
| excluding vault-specific files like workspaces and caches. | |
| Args: | |
| main_vault: Root of main vault | |
| other_vault: Root of other vault | |
| dry_run: If True, only show what would be done | |
| """ | |
| # Files to copy - str for vault root, tuple for (subfolder, filename) | |
| shared_files = [ | |
| (".obsidian", "app.json"), | |
| (".obsidian", "appearance.json"), | |
| (".obsidian", "community-plugins.json"), | |
| (".obsidian", "core-plugins.json"), | |
| (".obsidian", "hotkeys.json"), | |
| (".obsidian", "types.json"), | |
| ".export-ignore", | |
| ] | |
| # Directories to copy (subfolder, dirname) | |
| shared_dirs = [ | |
| (".obsidian", "plugins"), | |
| (".obsidian", "snippets"), | |
| (".obsidian", "themes"), | |
| ] | |
| # Copy individual files | |
| for file_spec in shared_files: | |
| if isinstance(file_spec, tuple): | |
| subfolder, filename = file_spec | |
| src = main_vault / subfolder / filename | |
| dst = other_vault / subfolder / filename | |
| display_name = f"{subfolder}/{filename}" | |
| else: | |
| src = main_vault / file_spec | |
| dst = other_vault / file_spec | |
| display_name = file_spec | |
| if src.exists(): | |
| if dry_run: | |
| print(f" would copy {display_name}") | |
| else: | |
| dst.parent.mkdir(parents=True, exist_ok=True) | |
| copy2(src, dst) | |
| # print(f" copied {display_name}") | |
| # Copy directories | |
| for subfolder, dirname in shared_dirs: | |
| src = main_vault / subfolder / dirname | |
| dst = other_vault / subfolder / dirname | |
| display_name = f"{subfolder}/{dirname}/" | |
| if src.exists(): | |
| if dst.exists(): | |
| if dry_run: | |
| print(f" would replace {display_name}") | |
| else: | |
| rmtree(dst) | |
| copytree(src, dst) | |
| # print(f" replaced {display_name}") | |
| elif dry_run: | |
| print(f" would copy {display_name}") | |
| else: | |
| dst.parent.mkdir(parents=True, exist_ok=True) | |
| copytree(src, dst) | |
| # print(f" copied {display_name}") | |
| def process_args(argv: list[str] | None = None) -> argparse.Namespace: | |
| """Process command line arguments.""" | |
| parser = argparse.ArgumentParser( | |
| description="Sync Obsidian config from main vault to others" | |
| ) | |
| parser.add_argument( | |
| "--main", | |
| type=Path, | |
| default=Path("~/data/2web/reagle.org/joseph/plan/ob-plan"), | |
| help="Main vault directory", | |
| ) | |
| parser.add_argument( | |
| "--dry-run", | |
| action="store_true", | |
| help="Show what would be done without making changes", | |
| ) | |
| return parser.parse_args(argv) | |
| def sync_templates(main_vault: Path, other_vault: Path, dry_run: bool = False) -> None: | |
| """Sync template files between vaults.""" | |
| templates_dir = "_templates" | |
| src = main_vault / templates_dir | |
| dst = other_vault / templates_dir | |
| if src.exists(): | |
| if dst.exists(): | |
| if dry_run: | |
| print(f" would sync {templates_dir}/") | |
| else: | |
| rmtree(dst) | |
| copytree(src, dst) | |
| elif dry_run: | |
| print(f" would copy {templates_dir}/") | |
| else: | |
| copytree(src, dst) | |
| def main() -> None: | |
| """Sync Obsidian configuration across vaults.""" | |
| args = process_args() | |
| main_vault = args.main.expanduser() | |
| others = [ | |
| Path(t).expanduser() | |
| for t in [ | |
| "~/data/2web/reagle.org/joseph/ob-codex", | |
| "~/data/2web/reagle.org/joseph/bl-codex", | |
| "~/data/2web/goatee.net/bl-goatee", | |
| "~/data/2misc/ob-misc", | |
| "~/data/mom/ob-mom", | |
| ] | |
| ] | |
| if not main_vault.exists(): | |
| print(f"Error: Main vault not found: {main_vault}") | |
| return | |
| print(f"Main vault: {main_vault}") | |
| if args.dry_run: | |
| print("DRY RUN - no changes will be made") | |
| for other in others: | |
| print(f"Syncing to {other.name}") | |
| sync_config(main_vault, other, args.dry_run) | |
| sync_templates(main_vault, other, args.dry_run) | |
| if __name__ == "__main__": | |
| main() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@mikeschlottig I just posted the current version of that script.