231 lines
8.5 KiB
Bash
Executable File
231 lines
8.5 KiB
Bash
Executable File
#!/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 <command> [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 <file> <jq_path> 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 <dir>."
|
|
|
|
# ── Docker run helper ─────────────────────────────────────────────────────────
|
|
|
|
run_python() {
|
|
# run_python <python_args...>
|
|
# 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 <command> [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
|