Track the source-of-truth code, skills, bootstrap rules, docs, and packaging files before adding new features. Exclude local vault config, generated skill mirrors, temporary exports, virtual environments, and business data files from version control. Validation: inspected staged file list and git ignored status before commit.
352 lines
9.6 KiB
Bash
Executable File
352 lines
9.6 KiB
Bash
Executable File
#!/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" <<EOF
|
|
OBSIDIAN_VAULT_PATH="$VAULT_DIR"
|
|
OBSIDIAN_WIKI_REPO="$INSTALL_DIR"
|
|
WIKI_ARCHIVE_SOURCES=true
|
|
OBSIDIAN_SOURCE_ARCHIVE_DIR=_sources/archive
|
|
WIKI_SOURCE_CATALOG=true
|
|
OBSIDIAN_SOURCE_CATALOG_DIR=_sources/catalog
|
|
OBSIDIAN_LINK_FORMAT=wikilink
|
|
OBSIDIAN_FILE_ASSET_DB=_meta/file-assets.sqlite
|
|
QMD_WIKI_COLLECTION=$QMD_WIKI_COLLECTION
|
|
QMD_TRANSPORT=cli
|
|
QMD_CLI_SEARCH_MODE=$QMD_CLI_SEARCH_MODE
|
|
QMD_CLI=qmd
|
|
EOF
|
|
|
|
cat > "$CONFIG_FILE" <<EOF
|
|
OBSIDIAN_VAULT_PATH="$VAULT_DIR"
|
|
OBSIDIAN_WIKI_REPO="$INSTALL_DIR"
|
|
OBSIDIAN_FILE_ASSET_DB="_meta/file-assets.sqlite"
|
|
QMD_WIKI_COLLECTION=$QMD_WIKI_COLLECTION
|
|
QMD_TRANSPORT=cli
|
|
QMD_CLI_SEARCH_MODE=$QMD_CLI_SEARCH_MODE
|
|
QMD_CLI=qmd
|
|
EOF
|
|
|
|
mkdir -p "$INSTALL_DIR/bin"
|
|
cat > "$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 <<EOF
|
|
|
|
Deployment complete.
|
|
|
|
Mother vault:
|
|
$VAULT_DIR
|
|
|
|
obsidian-wiki command wrapper:
|
|
$INSTALL_DIR/bin/obsidian-wiki-local
|
|
|
|
Examples:
|
|
$INSTALL_DIR/bin/obsidian-wiki-local file-index search "FH52K drawing" -n 10
|
|
$INSTALL_DIR/bin/obsidian-wiki-local bot-view audit
|
|
qmd search "FH52K replacement" -c "$QMD_WIKI_COLLECTION-sales-bot" -n 5
|
|
|
|
Important:
|
|
Point each robot at its own bot-view directory or QMD collection, not at the mother vault.
|
|
EOF
|