#!/usr/bin/env bash # # Deploy an obsidian-wiki Linux migration bundle on a headless server. # # Run this from the extracted bundle root: # sudo bash deploy-linux-server.sh --install-dir /opt/obsidian-wiki --vault-dir /srv/bjd-vault set -Eeuo pipefail IFS=$'\n\t' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BUNDLE_DIR="$SCRIPT_DIR" INSTALL_DIR="${INSTALL_DIR:-/opt/obsidian-wiki}" VAULT_DIR="${VAULT_DIR:-/srv/bjd-vault}" BOT_VIEW_DIR="${BOT_VIEW_DIR:-/srv/bjd-bot-views}" QMD_WIKI_COLLECTION="${QMD_WIKI_COLLECTION:-bjd-wiki}" QMD_CLI_SEARCH_MODE="${QMD_CLI_SEARCH_MODE:-balanced}" PROFILE_LIST="${PROFILE_LIST:-all}" SOURCE_FILE_PROFILES="${SOURCE_FILE_PROFILES:-quality-bot,engineering-bot}" CONFIG_FILE="${CONFIG_FILE:-$HOME/.obsidian-wiki/config}" RUN_DELETE=0 SKIP_FILE_INDEX=0 SKIP_BOT_VIEWS=0 SKIP_QMD=0 DRY_RUN=0 usage() { cat <<'EOF' Usage: bash deploy-linux-server.sh [options] Options: --install-dir DIR Destination for obsidian-wiki repo. Default: /opt/obsidian-wiki --vault-dir DIR Destination for mother vault. Default: /srv/bjd-vault --bot-view-dir DIR Destination for derived bot views. Default: /srv/bjd-bot-views --qmd-collection NAME Mother vault QMD collection name. Default: bjd-wiki --profiles LIST Comma-separated bot profiles to export, or "all". Default: all --source-file-profiles LIST Comma-separated profiles exported with --include-files. Default: quality-bot,engineering-bot --config-file PATH Config file to write. Default: ~/.obsidian-wiki/config --delete Use rsync --delete when copying repo/vault. --skip-file-index Do not rebuild _meta/file-assets.sqlite. --skip-bot-views Do not export bot views. --skip-qmd Do not create/update QMD collections. --dry-run Print planned actions only. -h, --help Show this help. Environment variables with the same names as the defaults may also be used: INSTALL_DIR, VAULT_DIR, BOT_VIEW_DIR, QMD_WIKI_COLLECTION, QMD_CLI_SEARCH_MODE, PROFILE_LIST, SOURCE_FILE_PROFILES, CONFIG_FILE EOF } die() { echo "ERROR: $*" >&2 exit 1 } info() { echo "[INFO] $*" } warn() { echo "[WARN] $*" >&2 } expand_path() { local path="$1" case "$path" in "~") printf '%s' "$HOME" ;; "~/"*) printf '%s/%s' "$HOME" "${path#~/}" ;; *) printf '%s' "$path" ;; esac } trim() { local value="$1" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" printf '%s' "$value" } csv_contains() { local needle="$1" local csv="$2" local item IFS=',' read -ra items <<< "$csv" for item in "${items[@]}"; do item="$(trim "$item")" [[ "$item" == "$needle" ]] && return 0 done return 1 } require_command() { local cmd="$1" command -v "$cmd" >/dev/null 2>&1 || die "missing command: $cmd" } while [[ $# -gt 0 ]]; do case "$1" in --install-dir) [[ $# -ge 2 ]] || die "--install-dir requires a directory" INSTALL_DIR="$2" shift 2 ;; --vault-dir) [[ $# -ge 2 ]] || die "--vault-dir requires a directory" VAULT_DIR="$2" shift 2 ;; --bot-view-dir) [[ $# -ge 2 ]] || die "--bot-view-dir requires a directory" BOT_VIEW_DIR="$2" shift 2 ;; --qmd-collection) [[ $# -ge 2 ]] || die "--qmd-collection requires a name" QMD_WIKI_COLLECTION="$2" shift 2 ;; --profiles) [[ $# -ge 2 ]] || die "--profiles requires a comma-separated list or all" PROFILE_LIST="$2" shift 2 ;; --source-file-profiles) [[ $# -ge 2 ]] || die "--source-file-profiles requires a comma-separated list" SOURCE_FILE_PROFILES="$2" shift 2 ;; --config-file) [[ $# -ge 2 ]] || die "--config-file requires a path" CONFIG_FILE="$2" shift 2 ;; --delete) RUN_DELETE=1 shift ;; --skip-file-index) SKIP_FILE_INDEX=1 shift ;; --skip-bot-views) SKIP_BOT_VIEWS=1 shift ;; --skip-qmd) SKIP_QMD=1 shift ;; --dry-run) DRY_RUN=1 shift ;; -h|--help) usage exit 0 ;; *) die "unknown option: $1" ;; esac done INSTALL_DIR="$(expand_path "$INSTALL_DIR")" VAULT_DIR="$(expand_path "$VAULT_DIR")" BOT_VIEW_DIR="$(expand_path "$BOT_VIEW_DIR")" CONFIG_FILE="$(expand_path "$CONFIG_FILE")" BUNDLE_REPO="$BUNDLE_DIR/obsidian-wiki" BUNDLE_VAULT="$BUNDLE_DIR/vault" [[ -d "$BUNDLE_REPO/obsidian_wiki" ]] || die "bundle repo missing: $BUNDLE_REPO" [[ -d "$BUNDLE_VAULT" ]] || die "bundle vault missing: $BUNDLE_VAULT" require_command python3 require_command rsync info "bundle: $BUNDLE_DIR" info "install dir: $INSTALL_DIR" info "vault dir: $VAULT_DIR" info "bot views: $BOT_VIEW_DIR" info "config file: $CONFIG_FILE" info "profiles: $PROFILE_LIST" info "source files: $SOURCE_FILE_PROFILES" if [[ "$DRY_RUN" -eq 1 ]]; then info "dry run complete; no files copied." exit 0 fi mkdir -p "$INSTALL_DIR" "$VAULT_DIR" "$BOT_VIEW_DIR" "$(dirname "$CONFIG_FILE")" RSYNC_ARGS=(-a) if [[ "$RUN_DELETE" -eq 1 ]]; then RSYNC_ARGS+=(--delete) fi info "copying obsidian-wiki repo..." rsync "${RSYNC_ARGS[@]}" "$BUNDLE_REPO/" "$INSTALL_DIR/" info "copying mother vault..." rsync "${RSYNC_ARGS[@]}" "$BUNDLE_VAULT/" "$VAULT_DIR/" cat > "$INSTALL_DIR/.env" < "$CONFIG_FILE" < "$INSTALL_DIR/bin/obsidian-wiki-local" <<'EOF' #!/usr/bin/env bash INSTALL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" export PYTHONPATH="$INSTALL_DIR${PYTHONPATH:+:$PYTHONPATH}" exec python3 -m obsidian_wiki "$@" EOF chmod +x "$INSTALL_DIR/bin/obsidian-wiki-local" ow() { PYTHONPATH="$INSTALL_DIR${PYTHONPATH:+:$PYTHONPATH}" python3 -m obsidian_wiki "$@" } if [[ "$SKIP_FILE_INDEX" -eq 0 ]]; then info "rebuilding file asset SQLite index..." ow file-index rebuild --vault "$VAULT_DIR" fi PROFILES=() if [[ "$PROFILE_LIST" == "all" ]]; then if PROFILE_OUTPUT="$(ow bot-view list --vault "$VAULT_DIR" 2>&1)"; then while IFS= read -r profile; do [[ -n "$profile" ]] && PROFILES+=("$profile") done <<< "$PROFILE_OUTPUT" else warn "bot profiles are not available; skipping bot-view audit/export." [[ -n "$PROFILE_OUTPUT" ]] && warn "$PROFILE_OUTPUT" SKIP_BOT_VIEWS=1 fi else IFS=',' read -ra raw_profiles <<< "$PROFILE_LIST" for profile in "${raw_profiles[@]}"; do profile="$(trim "$profile")" [[ -n "$profile" ]] && PROFILES+=("$profile") done fi if [[ "${#PROFILES[@]}" -gt 0 ]]; then info "auditing bot-view ACLs..." ow bot-view audit --vault "$VAULT_DIR" || warn "bot-view audit failed; continuing deployment." fi if [[ "$SKIP_BOT_VIEWS" -eq 0 ]]; then info "exporting bot views..." for profile in "${PROFILES[@]}"; do extra_args=() if csv_contains "$profile" "$SOURCE_FILE_PROFILES"; then extra_args+=(--include-files) fi ow bot-view export --vault "$VAULT_DIR" "$profile" --out "$BOT_VIEW_DIR" "${extra_args[@]}" done fi qmd_collection_exists() { qmd collection show "$1" >/dev/null 2>&1 } qmd_remove_if_exists() { local name="$1" if qmd_collection_exists "$name"; then qmd collection remove "$name" >/dev/null 2>&1 || true fi } qmd_add_collection_named() { local name="$1" local path="$2" local pattern="${3:-**/*.md}" qmd_remove_if_exists "$name" qmd collection add "$path" --name "$name" --mask "$pattern" } if [[ "$SKIP_QMD" -eq 0 ]]; then if command -v qmd >/dev/null 2>&1; then info "creating/updating QMD mother-vault collection..." qmd_add_collection_named "$QMD_WIKI_COLLECTION" "$VAULT_DIR" '**/*.md' if [[ "$SKIP_BOT_VIEWS" -eq 0 ]]; then for profile in "${PROFILES[@]}"; do bot_wiki="$BOT_VIEW_DIR/$profile/wiki" if [[ -d "$bot_wiki" ]]; then qmd_add_collection_named "$QMD_WIKI_COLLECTION-$profile" "$bot_wiki" '**/*.md' fi done fi info "refreshing QMD indexes..." qmd update info "refreshing QMD embeddings..." qmd embed -c "$QMD_WIKI_COLLECTION" || warn "QMD embed failed for $QMD_WIKI_COLLECTION" if [[ "$SKIP_BOT_VIEWS" -eq 0 ]]; then for profile in "${PROFILES[@]}"; do qmd embed -c "$QMD_WIKI_COLLECTION-$profile" || warn "QMD embed failed for $QMD_WIKI_COLLECTION-$profile" done fi else warn "qmd not found; skipped QMD collection rebuild. Install qmd, then rerun with --skip-file-index --skip-bot-views if only QMD is needed." fi fi cat <