#!/usr/bin/env bash # # Outline ↔ Obsidian Sync # Bash wrapper — delegates API/file work to Docker Python container, # handles git operations directly on the host. # # Usage: ./sync.sh [options] # # Commands (Phase 1): # init Initialize vault from Outline content # help Show this help # # Options: # --vault DIR Vault directory (overrides settings.json sync.vault_dir) # --settings FILE Path to settings file (default: ./settings.json) # -v Verbose Python output # -vv Debug Python output # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SETTINGS="$SCRIPT_DIR/settings.json" # ── Colours ─────────────────────────────────────────────────────────────────── GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # ── Helpers ─────────────────────────────────────────────────────────────────── die() { echo -e "${RED}✗ $*${NC}" >&2; exit 1; } info() { echo -e "${BLUE}$*${NC}"; } ok() { echo -e "${GREEN}✓ $*${NC}"; } warn() { echo -e "${YELLOW}⚠ $*${NC}"; } require_cmd() { command -v "$1" &>/dev/null || die "Required command not found: $1" } read_json() { # read_json e.g. read_json settings.json .sync.vault_dir jq -r "$2 // empty" "$1" 2>/dev/null || true } # ── Argument parsing ────────────────────────────────────────────────────────── COMMAND="${1:-help}" shift || true VAULT_ARG="" VERBOSE_FLAG="" while [[ $# -gt 0 ]]; do case "$1" in --vault) VAULT_ARG="$2"; shift 2 ;; --settings) SETTINGS="$2"; shift 2 ;; -vv) VERBOSE_FLAG="-vv"; shift ;; -v) VERBOSE_FLAG="-v"; shift ;; --verbose) VERBOSE_FLAG="-v"; shift ;; --) shift; break ;; *) shift ;; esac done # ── Config resolution ───────────────────────────────────────────────────────── [[ -f "$SETTINGS" ]] || die "settings.json not found at $SETTINGS" # Vault: CLI flag > settings.json > error if [[ -n "$VAULT_ARG" ]]; then VAULT_DIR="$VAULT_ARG" else VAULT_DIR="$(read_json "$SETTINGS" '.sync.vault_dir')" fi [[ -n "$VAULT_DIR" ]] || die \ "Vault directory not configured.\n" \ " Set sync.vault_dir in settings.json or pass --vault ." # ── Docker run helper ───────────────────────────────────────────────────────── run_python() { # run_python # Mounts SCRIPT_DIR (read-only) as /work and VAULT_DIR as /vault. docker run --rm \ --network domnet \ --user "$(id -u):$(id -g)" \ -e HOME=/tmp \ -v "$SCRIPT_DIR:/work:ro" \ -v "$VAULT_DIR:/vault" \ -w /work \ python:3.11-slim \ bash -c "pip install -qqq requests 2>/dev/null && python3 outline_sync.py $*" } # ── Commands ────────────────────────────────────────────────────────────────── cmd_init() { require_cmd git require_cmd docker require_cmd jq echo info "════════════════════════════════════════════════════════════" info " OUTLINE SYNC — init" info "════════════════════════════════════════════════════════════" echo echo " Vault: $VAULT_DIR" echo " Settings: $SETTINGS" echo # Guard: refuse if .git already exists if [[ -d "$VAULT_DIR/.git" ]]; then die "Vault is already initialized at $VAULT_DIR\n" \ " Remove the directory first, or use a different --vault path." fi # Create vault dir on the host BEFORE Docker mounts it. # If Docker creates the mount point itself it does so as root, # making the Python container (running as the host user) unable to write. mkdir -p "$VAULT_DIR" # ── Step 1: Python — export Outline content + write config files ────────── echo info "Step 1/3 Exporting Outline content..." echo if ! run_python "init --vault /vault --settings /work/settings.json $VERBOSE_FLAG"; then # Clean up incomplete vault so the user can retry cleanly if [[ -d "$VAULT_DIR" ]]; then warn "Export failed — removing incomplete vault directory." rm -rf "$VAULT_DIR" fi die "Export step failed. Check the error output above and retry." fi echo # ── Step 2: git init + first commit on outline branch ──────────────────── info "Step 2/3 Initializing git repository..." # Init the repo git -C "$VAULT_DIR" init --quiet # Rename initial branch to 'outline' before the first commit. # We use symbolic-ref for compatibility with git < 2.28 (no --initial-branch). git -C "$VAULT_DIR" symbolic-ref HEAD refs/heads/outline git -C "$VAULT_DIR" add --all TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)" git \ -C "$VAULT_DIR" \ -c user.name="outline-sync" \ -c user.email="sync@local" \ commit \ --quiet \ -m "sync: initial import from Outline @ $TIMESTAMP" ok "Committed to 'outline' branch" # Create 'main' at the same commit (this is the working branch for Obsidian) git -C "$VAULT_DIR" checkout --quiet -b main ok "Created 'main' branch" # ── Step 3: verify ──────────────────────────────────────────────────────── echo info "Step 3/3 Verifying..." local outline_sha main_sha outline_sha="$(git -C "$VAULT_DIR" rev-parse outline)" main_sha="$(git -C "$VAULT_DIR" rev-parse main)" if [[ "$outline_sha" == "$main_sha" ]]; then ok "Both branches at commit ${outline_sha:0:8}" else die "Branch mismatch after init:\n" \ " outline = $outline_sha\n" \ " main = $main_sha" fi local doc_count doc_count="$(find "$VAULT_DIR" -name "*.md" | wc -l | tr -d ' ')" ok "$doc_count markdown files committed" echo info "════════════════════════════════════════════════════════════" echo -e " ${GREEN}Vault ready at:${NC} $VAULT_DIR" echo echo " Next steps:" echo " 1. Open $VAULT_DIR as your Obsidian vault" echo " 2. Install the 'Obsidian Git' plugin" echo " 3. Configure it to auto-commit on the 'main' branch" info "════════════════════════════════════════════════════════════" echo } cmd_help() { echo "Outline ↔ Obsidian Sync" echo echo "Usage: $0 [options]" echo echo "Commands:" echo " init Initialize vault from Outline (Phase 1)" echo " help Show this help" echo echo "Options:" echo " --vault DIR Vault directory (overrides settings.json sync.vault_dir)" echo " --settings FILE Path to settings file (default: ./settings.json)" echo " -v Verbose output" echo " -vv Debug output" echo echo "Examples:" echo " $0 init" echo " $0 init --vault /data/my-vault" echo " $0 init -v" echo } # ── Dispatch ────────────────────────────────────────────────────────────────── case "$COMMAND" in init) cmd_init ;; help) cmd_help ;; *) die "Unknown command: '$COMMAND' — run '$0 help' for usage." ;; esac