Files
passepartout/passepartout

478 lines
21 KiB
Bash
Executable File

#!/bin/bash
set -e
PORT=9105
HOST="localhost"
RED='\033[0;31m'; GREEN='\033[0;32m'; BLUE='\033[0;34m'; YELLOW='\033[0;33m'; NC='\033[0m'
command_exists() { command -v "$1" >/dev/null 2>&1; }
# --- XDG PATH RESOLUTION ---
SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
SOURCE="$(readlink "$SOURCE")"
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
done
export SCRIPT_DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
export PASSEPARTOUT_CONFIG_DIR="$(realpath -m "${XDG_CONFIG_HOME:-$HOME/.config}/passepartout")"
export PASSEPARTOUT_DATA_DIR="${PASSEPARTOUT_DATA_DIR:-$(if [ -d "$HOME/memex/projects/passepartout/lisp" ]; then realpath -m "$HOME/memex/projects/passepartout"; else realpath -m "${XDG_DATA_HOME:-$HOME/.local/share}/passepartout"; fi)}"
export PASSEPARTOUT_STATE_DIR="$(realpath -m "${XDG_STATE_HOME:-$HOME/.local/state}/passepartout")"
export PASSEPARTOUT_BIN_DIR="$(realpath -m "${XDG_BIN_HOME:-$HOME/.local/bin}")"
export PASSEPARTOUT_MEMEX_DIR="${PASSEPARTOUT_MEMEX_DIR:-$HOME/memex}"
if [ -f "$PASSEPARTOUT_CONFIG_DIR/.env" ]; then
set -a; source "$PASSEPARTOUT_CONFIG_DIR/.env"; set +a
fi
# --- DISTRO DETECTION ---
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
debian|ubuntu|linuxmint|pop|elementary|zorin) echo "debian" ;;
fedora|rhel|centos|rocky|almalinux) echo "fedora" ;;
*) echo "unknown" ;;
esac
elif command_exists apt-get; then echo "debian"
elif command_exists dnf; then echo "fedora"
else echo "unknown"; fi
}
distro_install() {
local distro=$(detect_distro); shift
case "$distro" in
debian) sudo apt-get update && sudo apt-get install -y "$@" ;;
fedora) sudo dnf install -y "$@" ;;
*) echo "Unsupported distro. Install manually: sbcl emacs git curl socat"; return 1 ;;
esac
}
# --- DEPENDENCY CHECK ---
check_dependencies() {
local missing=()
for dep in sbcl git curl; do
if ! command_exists "$dep"; then missing+=("$dep"); fi
done
if ! command_exists emacs; then missing+=("emacs-nox"); fi
if [ ${#missing[@]} -gt 0 ]; then
echo -e "${YELLOW}--- Installing missing dependencies: ${missing[*]} ---${NC}"
local distro=$(detect_distro)
case "$distro" in
debian)
sudo apt-get update -qq 2>/dev/null || true
distro_install "${missing[@]}" 2>/dev/null || true
;;
fedora)
distro_install "${missing[@]}" 2>/dev/null || true
;;
esac
fi
}
# --- SETUP ---
setup_system() {
NON_INTERACTIVE=false; WITH_FIREWALL=false
for arg in "$@"; do
case "$arg" in
--non-interactive) NON_INTERACTIVE=true ;;
--with-firewall) WITH_FIREWALL=true ;;
esac
done
# Always deploy to XDG, not the dev directory
export PASSEPARTOUT_DATA_DIR="$(realpath -m "${XDG_DATA_HOME:-$HOME/.local/share}/passepartout")"
echo -e "${BLUE}=== Passepartout: Configure ===${NC}"
mkdir -p "$PASSEPARTOUT_CONFIG_DIR" "$PASSEPARTOUT_DATA_DIR" "$PASSEPARTOUT_STATE_DIR" "$PASSEPARTOUT_BIN_DIR"
mkdir -p "$PASSEPARTOUT_DATA_DIR/org" "$PASSEPARTOUT_DATA_DIR/lisp" "$PASSEPARTOUT_DATA_DIR/tests"
check_dependencies
if [ ! -d "$HOME/quicklisp" ]; then
echo -e "${YELLOW}--- Installing Quicklisp ---${NC}"
curl -O https://beta.quicklisp.org/quicklisp.lisp
sbcl --non-interactive --load quicklisp.lisp \
--eval "(quicklisp-quickstart:install)" \
--eval "(ql-util:without-prompting (ql:add-to-init-file))"
rm quicklisp.lisp
fi
echo -e "${YELLOW}--- Deploying Engine to $PASSEPARTOUT_DATA_DIR ---${NC}"
if [ "$SCRIPT_DIR" != "$PASSEPARTOUT_DATA_DIR" ]; then
cp "$SCRIPT_DIR/passepartout.asd" "$PASSEPARTOUT_DATA_DIR/"
fi
mkdir -p "$PASSEPARTOUT_DATA_DIR/org" "$PASSEPARTOUT_DATA_DIR/lisp" "$PASSEPARTOUT_DATA_DIR/tests"
export INSTALL_DIR="$PASSEPARTOUT_DATA_DIR"
# Tangle all org files into lisp/
for f in "$SCRIPT_DIR/org"/*.org; do
[ -f "$f" ] || continue
fname=$(basename "$f" .org)
echo "Tangling $fname..."
[ "$SCRIPT_DIR" != "$PASSEPARTOUT_DATA_DIR" ] && cp "$f" "$PASSEPARTOUT_DATA_DIR/org/"
(cd "$PASSEPARTOUT_DATA_DIR/org" && emacs -Q --batch \
--eval "(require 'org)" \
--eval "(setq org-confirm-babel-evaluate nil)" \
--eval "(org-babel-tangle-file \"${fname}.org\")") >/dev/null 2>&1 || true
done
# Move test files to tests/ directory
find "$PASSEPARTOUT_DATA_DIR/lisp" -name "*-tests.lisp" -exec mv {} "$PASSEPARTOUT_DATA_DIR/tests/" \; 2>/dev/null || true
ln -sf "$SCRIPT_DIR/passepartout" "$PASSEPARTOUT_BIN_DIR/passepartout"
if [ "$WITH_FIREWALL" = true ]; then
case $(detect_distro) in
debian) sudo ufw allow 9105/tcp 2>/dev/null && echo "✓ UFW: port 9105 opened" || true ;;
fedora) sudo firewall-cmd --add-port=9105/tcp --permanent 2>/dev/null && sudo firewall-cmd --reload 2>/dev/null && echo "✓ firewalld: port 9105 opened" || true ;;
esac
fi
# Pre-compile core + TUI so first daemon/TUI start is fast
echo -e "${YELLOW}--- Pre-compiling core system ---${NC}"
sbcl --noinform --load "$HOME/quicklisp/setup.lisp" \
--eval "(push (truename \"$PASSEPARTOUT_DATA_DIR/\") asdf:*central-registry*)" \
--eval '(ql:quickload :passepartout)' \
--eval '(ql:quickload :passepartout/tui :silent t)' \
--eval '(uiop:quit)' 2>&1 | grep -v '^;\|STYLE-WARNING\|WARNING: redefining' || true
if [ "$NON_INTERACTIVE" = true ]; then
echo "Configure complete."
exit 0
fi
echo -e "${YELLOW}--- Launching Setup Wizard ---${NC}"
exec sbcl --non-interactive \
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
--eval "(push (truename \"$PASSEPARTOUT_DATA_DIR/\") asdf:*central-registry*)" \
--eval '(ql:quickload :passepartout :force t)' \
--eval '(passepartout:skill-initialize-all)' \
--eval '(funcall (find-symbol "SETUP-WIZARD-RUN" :passepartout))'
}
# --- DOCTOR REPAIR ---
doctor_repair() {
echo -e "${BLUE}=== Passepartout: Repair Mode ===${NC}"
check_dependencies
mkdir -p "$PASSEPARTOUT_CONFIG_DIR" "$PASSEPARTOUT_DATA_DIR" "$PASSEPARTOUT_STATE_DIR" "$PASSEPARTOUT_BIN_DIR"
mkdir -p "$PASSEPARTOUT_DATA_DIR/org" "$PASSEPARTOUT_DATA_DIR/lisp" "$PASSEPARTOUT_DATA_DIR/tests"
for f in "$SCRIPT_DIR/org"/*.org; do
[ -f "$f" ] || continue
fname=$(basename "$f" .org)
echo " Checking $fname..."
if ! sbcl --non-interactive \
--eval "(load \"$PASSEPARTOUT_DATA_DIR/lisp/${fname}.lisp\")" \
--eval "(format t \"OK~%\")" 2>/dev/null | grep -q "OK"; then
echo " Re-tangling $fname.org..."
cp "$f" "$PASSEPARTOUT_DATA_DIR/org/"
(cd "$PASSEPARTOUT_DATA_DIR/org" && emacs -Q --batch \
--eval "(require 'org)" \
--eval "(setq org-confirm-babel-evaluate nil)" \
--eval "(org-babel-tangle-file \"${fname}.org\")") >/dev/null 2>&1 || true
fi
done
find "$PASSEPARTOUT_DATA_DIR/lisp" -name "*-tests.lisp" -exec mv {} "$PASSEPARTOUT_DATA_DIR/tests/" \; 2>/dev/null || true
echo -e "${GREEN}--- Repair Complete ---${NC}"
}
# --- INSTALL SKILL ---
install_skill() {
local SKILL_NAME=$1
if [ -z "$SKILL_NAME" ]; then
echo "Usage: passepartout install skill <skill-name>"
echo " Installs a skill from passepartout-contrib"
echo ""
echo "Available skills:"
if [ -d "$PASSEPARTOUT_MEMEX_DIR/projects/passepartout-contrib/skills" ]; then
ls "$PASSEPARTOUT_MEMEX_DIR/projects/passepartout-contrib/skills"/*.org 2>/dev/null | xargs -I{} basename {} .org | sed 's/org-skill-//' | sort | uniq
else
echo " (clone passepartout-contrib to ~/memex/projects/ first)"
fi
exit 1
fi
local SKILL_FILE="org-skill-${SKILL_NAME}.org"
local SOURCE_DIR="$PASSEPARTOUT_MEMEX_DIR/projects/passepartout-contrib/skills"
local TARGET_DIR="$PASSEPARTOUT_DATA_DIR/skills"
if [ ! -d "$SOURCE_DIR" ]; then
echo "Error: Contrib skills not found at $SOURCE_DIR"
echo "Run: git clone https://github.com/amrgharbeia/passepartout-contrib.git \$PASSEPARTOUT_MEMEX_DIR/projects/passepartout-contrib"
exit 1
fi
if [ ! -f "$SOURCE_DIR/$SKILL_FILE" ]; then
echo "Error: Skill '$SKILL_NAME' not found"
exit 1
fi
mkdir -p "$TARGET_DIR"
cp "$SOURCE_DIR/$SKILL_FILE" "$TARGET_DIR/"
(cd "$TARGET_DIR" && emacs -Q --batch \
--eval "(require 'org)" \
--eval "(setq org-confirm-babel-evaluate nil)" \
--eval "(org-babel-tangle-file \"$SKILL_FILE\")") >/dev/null 2>&1 || true
rm -f "$TARGET_DIR/$SKILL_FILE"
if [ -f "$TARGET_DIR/${SKILL_NAME}-tests.lisp" ]; then
mv "$TARGET_DIR/${SKILL_NAME}-tests.lisp" "$PASSEPARTOUT_DATA_DIR/tests/" 2>/dev/null || true
fi
echo "Skill '$SKILL_NAME' installed. Restart to activate."
}
# --- INSTALL SERVICE ---
install_service() {
mkdir -p "$HOME/.config/systemd/user"
cat > "$HOME/.config/systemd/user/passepartout.service" << 'SERVICEEOF'
[Unit]
Description=Passepartout Daemon
After=network.target
[Service]
Type=simple
ExecStart=%h/projects/passepartout/passepartout.sh daemon
Restart=on-failure
RestartSec=10
WorkingDirectory=%h/projects/passepartout
[Install]
WantedBy=default.target
SERVICEEOF
systemctl --user daemon-reload
systemctl --user enable passepartout.service
systemctl --user start passepartout.service
echo -e "${GREEN}✓ passepartout.service installed and started${NC}"
echo " Status: systemctl --user status passepartout.service"
echo " Logs: journalctl --user -u passepartout.service -f"
}
uninstall_service() {
systemctl --user stop passepartout.service 2>/dev/null || true
systemctl --user disable passepartout.service 2>/dev/null || true
rm -f "$HOME/.config/systemd/user/passepartout.service"
systemctl --user daemon-reload
echo -e "${GREEN}✓ passepartout.service removed${NC}"
}
# --- BACKUP ---
backup() {
local dest="${1:-$HOME/passepartout-backup-$(date +%Y%m%d-%H%M%S).tar.gz}"
if [ -f "$dest" ]; then echo "Error: $dest exists"; exit 1; fi
echo "Backing up to $dest..."
tar -czf "$dest" \
"$PASSEPARTOUT_CONFIG_DIR" "$PASSEPARTOUT_DATA_DIR" \
"$PASSEPARTOUT_MEMEX_DIR/gtd.org" "$PASSEPARTOUT_MEMEX_DIR/projects/passepartout" \
2>/dev/null || true
echo -e "${GREEN}✓ Backed up to $dest${NC}"
}
restore() {
local src="$1"
if [ -z "$src" ] || [ ! -f "$src" ]; then
echo "Usage: passepartout restore <backup-file>"
exit 1
fi
echo "Restoring from $src..."
tar -xzf "$src" -C /
echo -e "${GREEN}✓ Restored. Run 'passepartout doctor' to verify.${NC}"
}
# --- HELP ---
help() {
echo ""
echo "Passepartout — Your Autonomous, Plain-Text Life Assistant"
echo ""
echo "Usage: passepartout <command> [options]"
echo ""
echo "System:"
echo " configure [--non-interactive] [--with-firewall] Install or reconfigure the system"
echo " setup Alias for configure"
echo " doctor [--fix] [--watch] System health check"
echo ""
echo "Running:"
echo " daemon Start background daemon"
echo " tui Launch terminal UI"
echo " gateway {link|unlink|list} <platform> <token> Manage chat gateways"
echo ""
echo "Skills:"
echo " install skill <name> Install a skill from contrib"
echo " install service Install systemd service (auto-start)"
echo " uninstall service Remove systemd service"
echo ""
echo "Data:"
echo " backup [path] Backup config, data, memex"
echo " restore <path> Restore from a backup"
echo ""
echo "Quick start:"
echo " curl -fsSL https://raw.githubusercontent.com/amrgharbeia/passepartout/main/passepartout.sh | bash -s configure"
echo ""
}
# --- COMMAND ROUTER ---
COMMAND=$1; [ -z "$COMMAND" ] && COMMAND="help"
shift || true
case "$COMMAND" in
configure|setup)
check_dependencies
if [ "$1" = "--add-provider" ]; then
sbcl --non-interactive \
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
--eval "(push (truename \"$PASSEPARTOUT_DATA_DIR/\") asdf:*central-registry*)" \
--eval '(ql:quickload :passepartout :force t)' \
--eval '(passepartout:skill-initialize-all)' \
--eval '(funcall (find-symbol "SETUP-PROVIDER-ADD" :passepartout))'
elif [ "$1" = "--link" ]; then
exec "$0" gateway link "$2" "$3"
else
setup_system "$@"
fi
;;
doctor)
check_dependencies
if [ "$1" = "--watch" ]; then
while true; do
echo "--- $(date '+%Y-%m-%d %H:%M:%S') ---"
sbcl --non-interactive \
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
--eval "(push (truename \"$PASSEPARTOUT_DATA_DIR/\") asdf:*central-registry*)" \
--eval '(ql:quickload :passepartout :force t)' \
--eval '(passepartout:skill-initialize-all)' \
--eval '(funcall (find-symbol "DIAGNOSTICS-RUN-ALL" :passepartout))' 2>&1 | grep -E "(HEALTH|OK|FAIL|WARN|SYSTEM|===)" || true
sleep 60
done
elif [ "$1" = "--fix" ]; then
if [ ! -f "$PASSEPARTOUT_DATA_DIR/harness/package.lisp" ] || [ ! -f "$PASSEPARTOUT_DATA_DIR/harness/skills.lisp" ]; then
setup_system "$@"
else
doctor_repair
fi
else
exec sbcl --non-interactive \
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
--eval "(push (truename \"$PASSEPARTOUT_DATA_DIR/\") asdf:*central-registry*)" \
--eval '(ql:quickload :passepartout :force t)' \
--eval '(passepartout:skill-initialize-all)' \
--eval '(funcall (find-symbol "DIAGNOSTICS-MAIN" :passepartout))'
fi
;;
daemon)
check_dependencies
export PASSEPARTOUT_DATA_DIR="${PASSEPARTOUT_DATA_DIR:-$SCRIPT_DIR}"
export MEMEX_DIR="${PASSEPARTOUT_MEMEX_DIR:-$HOME/memex}"
echo "Starting daemon (data dir: $PASSEPARTOUT_DATA_DIR)..."
nohup sbcl --non-interactive \
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
--eval '(ql:quickload :passepartout)' \
--eval "(handler-case (load (format nil \"~alisp/neuro-router.lisp\" (truename \"$PASSEPARTOUT_DATA_DIR/\"))) (error () nil))" \
--eval "(handler-case (load (format nil \"~alisp/embedding-backends.lisp\" (truename \"$PASSEPARTOUT_DATA_DIR/\"))) (error () nil))" \
--eval "(handler-case (load (format nil \"~alisp/neuro-explorer.lisp\" (truename \"$PASSEPARTOUT_DATA_DIR/\"))) (error () nil))" \
--eval '(funcall (find-symbol "MAIN" :passepartout))' \
> "$PASSEPARTOUT_STATE_DIR/daemon.log" 2>&1 &
echo "Waiting for port 9105..."
for i in $(seq 1 120); do
if ss -tln 2>/dev/null | grep -q 9105 || netstat -tln 2>/dev/null | grep -q 9105; then
echo "✓ Daemon ready on port 9105"; exit 0
fi
sleep 1
done
echo "✗ Daemon failed to start. Check $PASSEPARTOUT_STATE_DIR/daemon.log"; exit 1
;;
tui)
check_dependencies
export PASSEPARTOUT_DATA_DIR="${PASSEPARTOUT_DATA_DIR:-$SCRIPT_DIR}"
if ! ss -tln 2>/dev/null | grep -q 9105 && ! netstat -tln 2>/dev/null | grep -q 9105; then
echo "Starting daemon first..."
$0 daemon
fi
# Build TUI load script with proper paths
cat > /tmp/tui-load.lisp << LISPEOF
(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))
(declaim (optimize (debug 3) (speed 0) (safety 3)))
(push (truename "$PASSEPARTOUT_DATA_DIR/") asdf:*central-registry*)
(ql:quickload :cl-tty :silent t)
(ql:quickload :passepartout :silent t)
(let ((dir (pathname (format nil "~a/lisp/" (truename "$PASSEPARTOUT_DATA_DIR")))))
(dolist (f '("channel-tui-state" "channel-tui-view" "channel-tui-main"))
(let* ((src (merge-pathnames (format nil "~a.lisp" f) dir))
(fasl (merge-pathnames (format nil "~a.fasl" f) dir)))
(when (or (not (probe-file fasl))
(< (file-write-date fasl) (file-write-date src)))
(compile-file src :output-file fasl :verbose nil :print nil))
(load fasl :verbose nil :print nil))))
(in-package :passepartout)
(handler-bind ((error (lambda (c) (ignore-errors
(with-open-file (f (merge-pathnames ".cache/passepartout/tui-crash.log" (user-homedir-pathname))
:direction :output :if-exists :supersede :if-does-not-exist :create)
(format f "CRASH: ~a~%~%" c) (sb-debug:print-backtrace :count 50 :stream f) (finish-output f)))
(format t "~%=== TUI CRASH ===~%CRASH: ~a~%" c)
(format t "Full backtrace saved to ~~/.cache/passepartout/tui-crash.log~%")
(sleep 3) (finish-output) (uiop:quit 1))))
(passepartout.channel-tui:tui-main))
LISPEOF
# Capture terminal dimensions in non-standard env vars
# (SBCL strips COLUMNS/LINES but leaves MY_* alone).
ts=$(stty size 2>/dev/null)
export MY_TERM_ROWS="${ts%% *}" MY_TERM_COLS="${ts##* }"
# Clear stale cl-tty cache to ensure latest backend-size fixes
find ~/.cache/common-lisp -name "*.fasl" -path "*cl-tty*" -delete 2>/dev/null
exec sbcl --noinform --load /tmp/tui-load.lisp
;;
gateway)
SUBCMD=$1; PLATFORM=$2; TOKEN=$3
check_dependencies
case "$SUBCMD" in
list)
exec sbcl --non-interactive \
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
--eval "(push (truename \"$PASSEPARTOUT_DATA_DIR/\") asdf:*central-registry*)" \
--eval '(ql:quickload :passepartout :force t)' \
--eval '(passepartout:skill-initialize-all)' \
--eval '(funcall (find-symbol "MESSAGING-LIST-PRINT" (find-package "OPENCORTEX.SKILLS.ORG-SKILL-GATEWAY-MESSAGING")))'
;;
link)
[ -z "$PLATFORM" ] || [ -z "$TOKEN" ] && echo "Usage: passepartout gateway link <platform> <token>" && exit 1
exec sbcl --non-interactive \
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
--eval "(push (truename \"$PASSEPARTOUT_DATA_DIR/\") asdf:*central-registry*)" \
--eval '(ql:quickload :passepartout :force t)' \
--eval '(passepartout:skill-initialize-all)' \
--eval "(funcall (find-symbol \"MESSAGING-LINK\" (find-package \"OPENCORTEX.SKILLS.ORG-SKILL-GATEWAY-MESSAGING\")) \"$PLATFORM\" \"$TOKEN\")"
;;
unlink)
[ -z "$PLATFORM" ] && echo "Usage: passepartout gateway unlink <platform>" && exit 1
exec sbcl --non-interactive \
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
--eval "(push (truename \"$PASSEPARTOUT_DATA_DIR/\") asdf:*central-registry*)" \
--eval '(ql:quickload :passepartout :force t)' \
--eval '(passepartout:skill-initialize-all)' \
--eval "(funcall (find-symbol \"MESSAGING-UNLINK\" (find-package \"OPENCORTEX.SKILLS.ORG-SKILL-GATEWAY-MESSAGING\")) \"$PLATFORM\")"
;;
*) echo "Usage: passepartout gateway {list|link|unlink}"; exit 1 ;;
esac
;;
install)
case "$1" in
skill) shift; install_skill "$@" ;;
service) install_service ;;
*) echo "Usage: passepartout install {skill|service}" >&2; exit 1 ;;
esac
;;
uninstall)
case "$1" in
service) uninstall_service ;;
*) echo "Usage: passepartout uninstall {service}" >&2; exit 1 ;;
esac
;;
backup)
backup "$1"
;;
restore)
restore "$1"
;;
help|--help|-h)
help
;;
*)
echo "Unknown command: $COMMAND"
help
exit 1
;;
esac