# Todoist Deploy Script One-shot installer that sets up the venv, config, and launchd agent for the Todoist sync. Save as `deploy-todoist-sync.sh` (see [[09 Todoist Integration]]). ```bash #!/usr/bin/env bash # ===================================================================== # Deploy the Obsidian <-> Todoist sync for a single vault. # ===================================================================== # The vault syncs its Activities/Tasks/ folder to a Todoist project # (the project name below must match the one in Todoist). # # The sync runs as a launchd agent with its own config dir, state file, # log, and sync token. # # Safe to re-run (idempotent): # * the venv is only created if missing # * an existing config.json is LEFT AS-IS (your token is never clobbered) # * the launchd agent is unloaded + reloaded each run so plist/script # edits take effect # # Usage: # ./deploy-todoist-sync.sh # prompts for the token # TODOIST_TOKEN=abc123 ./deploy-todoist-sync.sh # non-interactive # # NOTE: the project is also the DEFAULT_PROJECT_NAME baked into # todoist_sync.py, so the project_name written below is just a # belt-and-suspenders confirmation. # ===================================================================== set -euo pipefail VENV="$HOME/.venvs/todoist-sync" ICLOUD="$HOME/Library/Mobile Documents/iCloud~md~obsidian/Documents" LAUNCH_AGENTS="$HOME/Library/LaunchAgents" # vault-folder | config-name | todoist-project | launchd-label # # To sync MULTIPLE vaults (one Todoist project per vault), add one row # per vault — each gets its own config dir, state file, log, and agent. # Example for three vaults: # VAULTS=( # "MyVault|obsidian-todoist-sync|Obsidian|local.obsidian-todoist-sync" # "WorkVault|work-todoist-sync|Work|local.work-todoist-sync" # "HomeVault|home-todoist-sync|Home|local.home-todoist-sync" # ) VAULTS=( "YOUR_VAULT|obsidian-todoist-sync|Obsidian|local.obsidian-todoist-sync" ) # --- 1. Todoist API token --- TOKEN="${TODOIST_TOKEN:-}" if [ -z "$TOKEN" ]; then read -r -s -p "Paste your Todoist API token (Todoist > Settings > Integrations > Developer): " TOKEN echo fi [ -n "$TOKEN" ] || { echo "No token provided; aborting." >&2; exit 1; } # --- 2. venv (created once; the agent uses this Python binary) --- if [ ! -x "$VENV/bin/python" ]; then command -v uv >/dev/null 2>&1 || { echo "uv is not installed. Install it first: brew install uv" >&2 exit 1 } echo "Creating venv at $VENV ..." uv venv --python 3.12 "$VENV" uv pip install --python "$VENV/bin/python" requests watchdog python-frontmatter else echo "venv already present at $VENV" fi mkdir -p "$LAUNCH_AGENTS" # --- 3. Per-vault config + launchd agent --- for entry in "${VAULTS[@]}"; do IFS='|' read -r vault cfg proj label <<<"$entry" scripts="$ICLOUD/$vault/Scripts" cfgdir="$HOME/.config/$cfg" plist_src="$scripts/$label.plist" plist_dst="$LAUNCH_AGENTS/$label.plist" if [ ! -f "$plist_src" ]; then echo "[$vault] SKIP — plist not found: $plist_src" >&2 continue fi mkdir -p "$cfgdir" if [ -f "$cfgdir/config.json" ]; then echo "[$vault] config.json already exists — leaving token as-is" else cat >"$cfgdir/config.json" <<EOF { "todoist_token": "$TOKEN", "project_name": "$proj" } EOF chmod 600 "$cfgdir/config.json" echo "[$vault] wrote $cfgdir/config.json (project: $proj)" fi cp "$plist_src" "$plist_dst" launchctl unload "$plist_dst" 2>/dev/null || true launchctl load "$plist_dst" echo "[$vault] loaded launchd agent: $label" done cat <<EOF Done. Verify it is running (it should show a numeric PID): launchctl list | grep todoist-sync If the row shows "-" instead of a PID, check the stderr log, e.g.: cat ~/.config/obsidian-todoist-sync/stderr.log IMPORTANT — one-time Full Disk Access grant: launchd-spawned processes can't read iCloud Drive until you grant FDA to the venv's python binary: System Settings > Privacy & Security > Full Disk Access > + then Cmd-Shift-G and paste: $VENV/bin/python Toggle it on, then re-run this script (or reload the agent). FIRST-RUN NOTE: on first start, every Task note in the vault that has no todoist_id: yet becomes a NEW Todoist task in the project. Delete or archive any Task notes you don't want pushed before running. EOF ```