#!/usr/bin/env bash # syntaxis Bundle Installer # # Standalone bash installer for offline bundles # Works without NuShell or Rust toolchain # # Usage: # ./install.sh # Interactive mode # ./install.sh --prefix ~/.local # Custom prefix # ./install.sh --verify --force # Auto-verify and force overwrite # ./install.sh --help # Show help set -euo pipefail # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Script state BUNDLE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(pwd)" BUNDLE_NAME=$(basename "$BUNDLE_DIR") INSTALL_PREFIX="" VERIFY=false FORCE=false BACKUP=true DRY_RUN=false HELP=false UNATTENDED=false # Defaults by OS detect_os() { case "$(uname -s)" in Darwin*) echo "macos" ;; Linux*) echo "linux" ;; MINGW*) echo "windows" ;; *) echo "unknown" ;; esac } detect_arch() { case "$(uname -m)" in x86_64) echo "x86_64" ;; aarch64) echo "aarch64" ;; arm64) echo "aarch64" ;; # macOS Apple Silicon *) echo "unknown" ;; esac } # Logging functions log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" } log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2 } log_success() { echo -e "${GREEN}✅${NC} $*" } log_info() { echo -e "${BLUE}ℹ️${NC} $*" } log_warn() { echo -e "${YELLOW}⚠️${NC} $*" } # Check if NuShell is installed check_nushell() { if command -v nu &> /dev/null; then NUSHELL_VERSION=$(nu --version 2>/dev/null | head -1) return 0 else return 1 fi } # Show NuShell installation guide show_nushell_guide() { local os=$(detect_os) echo "" echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BLUE}📦 How to Install NuShell${NC}" echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" case "$os" in macos) echo "macOS (Homebrew):" echo " brew install nushell" echo "" echo "macOS (Cargo):" echo " cargo install nu" ;; linux) echo "Linux (Arch):" echo " pacman -S nushell" echo "" echo "Linux (Ubuntu/Debian):" echo " cargo install nu" echo "" echo "Linux (Fedora):" echo " dnf install nushell" ;; windows) echo "Windows (WinGet):" echo " winget install nushell" echo "" echo "Windows (Cargo):" echo " cargo install nu" ;; *) echo "See installation guide:" echo " https://www.nushell.sh/book/installation.html" ;; esac echo "" echo "After installation:" echo " 1. Re-run this installer to use full-featured wrappers" echo " 2. Run: ./install.sh" echo "" } # Prompt user about NuShell prompt_nushell() { if [[ $UNATTENDED == true ]]; then # In unattended mode, just warn and continue log_warn "NuShell not found - using simplified wrapper mode" return 0 fi echo "" echo -e "${YELLOW}⚠️ NuShell Not Detected${NC}" echo "" echo "Wrappers will use simplified mode (config auto-discovery only)" echo "For full features, install NuShell: https://www.nushell.sh" echo "" while true; do echo -n "Continue with simplified wrappers? [Y/n/i] " read -r response case "$response" in [Yy]|"") return 0 ;; [Nn]) log_info "Please install NuShell and run this installer again" show_nushell_guide exit 0 ;; [Ii]) show_nushell_guide echo -n "Continue with installation? [Y/n] " read -r cont if [[ "$cont" =~ [Yy] ]] || [[ -z "$cont" ]]; then return 0 else exit 0 fi ;; *) echo "Please answer Y (continue), N (exit), or I (installation guide)" ;; esac done } # Display help show_help() { cat << 'EOF' syntaxis Bundle Installer USAGE: ./install.sh [OPTIONS] OPTIONS: --prefix Installation prefix Default: /usr/local (Linux/macOS) or /Program Files/syntaxis (Windows) --config-dir Configuration directory Default: ~/.config/syntaxis --verify Verify checksums before installing --backup Backup existing binaries (default: true) --no-backup Don't backup existing binaries --force Force overwrite without prompting --unattended Non-interactive mode (use defaults) --dry-run Show what would be installed --help Show this help message EXAMPLES: # Interactive installation (prompts for options) ./install.sh # Install to custom location with verification ./install.sh --prefix ~/.local --verify # Force installation without backups ./install.sh --force --no-backup # Unattended installation ./install.sh --unattended --prefix /opt/syntaxis # Preview installation ./install.sh --dry-run NOTES: - This script should be run from within the extracted bundle directory - Bundles include all necessary files: binaries, configs, and documentation - Installation manifests are saved to ~/.syntaxis/ - Existing binaries are backed up with timestamp suffix EOF } # Parse command-line arguments parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --prefix) INSTALL_PREFIX="$2" shift 2 ;; --config-dir) CONFIG_DIR="$2" shift 2 ;; --verify) VERIFY=true shift ;; --backup) BACKUP=true shift ;; --no-backup) BACKUP=false shift ;; --force) FORCE=true shift ;; --unattended) UNATTENDED=true shift ;; --dry-run) DRY_RUN=true shift ;; --help|-h) HELP=true shift ;; *) log_error "Unknown option: $1" exit 1 ;; esac done } # Validate bundle structure validate_bundle() { log_info "Validating bundle structure..." local required_dirs=("bin" "configs" "docs") local missing_dirs=() for dir in "${required_dirs[@]}"; do if [[ ! -d "$BUNDLE_DIR/$dir" ]]; then missing_dirs+=("$dir") fi done if [[ ${#missing_dirs[@]} -gt 0 ]]; then log_error "Missing required directories: ${missing_dirs[*]}" return 1 fi # Check for manifest if [[ ! -f "$BUNDLE_DIR/manifest.toml" ]]; then log_warn "manifest.toml not found (may affect verification)" fi log_success "Bundle structure valid" return 0 } # Calculate SHA256 checksum sha256_file() { local file="$1" if command -v sha256sum &> /dev/null; then sha256sum "$file" | awk '{print $1}' elif command -v shasum &> /dev/null; then shasum -a 256 "$file" | awk '{print $1}' elif command -v openssl &> /dev/null; then openssl dgst -sha256 "$file" | awk '{print $NF}' else log_warn "No checksum command available, skipping verification" return 1 fi } # Verify checksums from manifest verify_checksums() { if [[ ! -f "$BUNDLE_DIR/manifest.toml" ]]; then log_warn "manifest.toml not found, skipping checksum verification" return 0 fi log_info "Verifying checksums..." local errors=0 local verified=0 # Parse checksums from TOML (basic parsing) while IFS='=' read -r key value; do if [[ $key =~ ^[[:space:]]*\"bin/ ]]; then local file_path=$(echo "$key" | sed 's/.*"\(bin\/[^"]*\)".*/\1/') local expected=$(echo "$value" | sed 's/.*"\([^"]*\)".*/\1/') if [[ -f "$BUNDLE_DIR/$file_path" ]]; then local actual=$(sha256_file "$BUNDLE_DIR/$file_path") if [[ "$actual" == "$expected" ]]; then verified=$((verified + 1)) else log_warn "Checksum mismatch: $file_path" errors=$((errors + 1)) fi fi fi done < "$BUNDLE_DIR/manifest.toml" if [[ $errors -gt 0 ]]; then log_warn "Checksum verification failed for $errors file(s)" return 1 fi if [[ $verified -gt 0 ]]; then log_success "Verified $verified file(s) with checksums" fi return 0 } # Detect installation prefix detect_install_prefix() { local os=$(detect_os) local default_prefix case "$os" in linux|macos) # Try /usr/local first, fallback to ~/.local if [[ -w /usr/local/bin ]]; then default_prefix="/usr/local" else default_prefix="$HOME/.local" fi ;; windows) default_prefix="C:\\Program Files\\syntaxis" ;; *) default_prefix="$HOME/.local" ;; esac if [[ -z "$INSTALL_PREFIX" ]]; then INSTALL_PREFIX="$default_prefix" fi # Expand ~ to home INSTALL_PREFIX=$(eval echo "$INSTALL_PREFIX") # Create bin directory path BIN_DIR="$INSTALL_PREFIX/bin" } # Ensure directory exists and is writable ensure_directory() { local dir="$1" local description="$2" if [[ ! -d "$dir" ]]; then log_info "Creating $description: $dir" mkdir -p "$dir" || { log_error "Failed to create $description: $dir" return 1 } fi # Check write permissions if [[ ! -w "$dir" ]]; then log_error "No write permission to $description: $dir" return 1 fi return 0 } # Backup existing binary backup_binary() { local binary_path="$1" if [[ ! -f "$binary_path" ]]; then return 0 # No backup needed if file doesn't exist fi local timestamp=$(date +%Y%m%d_%H%M%S) local backup_path="${binary_path}.backup.${timestamp}" log_info "Backing up: $(basename "$binary_path") → $(basename "$backup_path")" cp "$binary_path" "$backup_path" || { log_error "Failed to backup $binary_path" return 1 } return 0 } # Create hybrid bash wrapper script for a binary # Tries NuShell first (full features), falls back to bash (simplified config discovery) create_bash_wrapper() { local dest="$1" local binary_name=$(basename "$dest") cat > "$dest" << 'WRAPPER_EOF' #!/bin/bash # Hybrid wrapper for BINARY_NAME # Layer 1: Bash wrapper # - Tries NuShell first (full features with libraries) # - Falls back to bash (simplified config discovery) # # Usage: BINARY_NAME [arguments] BINARY_NAME="BINARY_PLACEHOLDER" REAL_BINARY="$(dirname "$0")/${BINARY_NAME}.real" # Try NuShell first (preferred - full features) if command -v nu &>/dev/null; then # NuShell available - use full wrapper with libraries export NU_LIB_DIRS="$HOME/.config/syntaxis/scripts" exec nu "$HOME/.config/syntaxis/scripts/${BINARY_NAME}.nu" "$@" fi # Bash fallback (simplified - config discovery only) # Search for config file in standard locations for cfg_path in \ "$HOME/.config/syntaxis/${BINARY_NAME}.toml" \ "$HOME/.config/syntaxis/config.toml" \ "$HOME/.config/syntaxis/syntaxis.toml" \ ".syntaxis/${BINARY_NAME}.toml" \ ".project/${BINARY_NAME}.toml" \ ".coder/${BINARY_NAME}.toml"; do if [[ -f "$cfg_path" ]]; then exec "$REAL_BINARY" --config "$cfg_path" "$@" fi done # No config found - execute with defaults exec "$REAL_BINARY" "$@" WRAPPER_EOF # Replace placeholder with actual binary name sed -i.tmp "s/BINARY_PLACEHOLDER/${binary_name}/g" "$dest" rm -f "${dest}.tmp" chmod 755 "$dest" } # Install binary with wrapper support install_binary() { local source="$1" local dest="$2" local binary_name=$(basename "$source") local real_dest="${dest}.real" if [[ ! -f "$source" ]]; then log_warn "Binary not found: $binary_name" return 1 fi # Check if destination exists if [[ -f "$dest" ]] && [[ $FORCE == false ]] && [[ $UNATTENDED == false ]]; then echo -n "Binary already exists: $binary_name. Overwrite? (y/n) " read -r response if [[ $response != [yY] ]]; then log_info "Skipped: $binary_name" return 0 fi fi # Backup if requested if [[ $BACKUP == true ]]; then backup_binary "$dest" || true # Don't fail if no backup backup_binary "$real_dest" || true # Also backup .real if it exists fi # Copy binary to .real cp "$source" "$real_dest" || { log_error "Failed to install: $binary_name" return 1 } # Make .real binary executable (Unix) if [[ $(uname -s) != "MINGW"* ]]; then chmod 755 "$real_dest" fi # Create bash wrapper at original location create_bash_wrapper "$dest" log_success "Installed: $binary_name (with wrapper)" log_info " Real binary: $real_dest" log_info " Wrapper: $dest" return 0 } # Install all binaries install_binaries() { log_info "Installing binaries..." if [[ ! -d "$BUNDLE_DIR/bin" ]]; then log_error "No bin directory found in bundle" return 1 fi local binaries=() local failed=0 # Find all binaries for binary in "$BUNDLE_DIR/bin"/*; do if [[ -f "$binary" ]] && [[ -x "$binary" ]]; then binaries+=("$(basename "$binary")") fi done if [[ ${#binaries[@]} -eq 0 ]]; then log_error "No binaries found in bundle/bin" return 1 fi # Install each binary for binary in "${binaries[@]}"; do if ! install_binary "$BUNDLE_DIR/bin/$binary" "$BIN_DIR/$binary"; then failed=$((failed + 1)) fi done if [[ $failed -gt 0 ]]; then log_error "$failed binary(ies) failed to install" return 1 fi log_success "All binaries installed (${#binaries[@]} total)" return 0 } # Deploy NuShell wrapper scripts deploy_wrapper_scripts() { local scripts_dir="$BUNDLE_DIR/scripts" local config_scripts_dir="$HOME/.config/syntaxis/scripts" # Check if scripts directory exists in bundle if [[ ! -d "$scripts_dir" ]]; then log_warn "No scripts directory found in bundle (wrapper scripts not available)" return 0 fi log_info "Deploying wrapper scripts to $config_scripts_dir..." # Create wrapper scripts directory if ! ensure_directory "$config_scripts_dir" "wrapper scripts directory"; then log_warn "Could not create wrapper scripts directory" return 0 fi local deployed=0 local failed=0 # Deploy shared library if [[ -f "$scripts_dir/syntaxis-lib.nu" ]]; then cp "$scripts_dir/syntaxis-lib.nu" "$config_scripts_dir/" || { log_warn "Failed to deploy syntaxis-lib.nu" failed=$((failed + 1)) } && { deployed=$((deployed + 1)) log_info " ✅ Deployed syntaxis-lib.nu" } fi # Deploy binary-specific wrappers (cli, tui, api) for binary in syntaxis-cli syntaxis-tui syntaxis-api; do # Deploy binary wrapper script (e.g., syntaxis-cli.nu) if [[ -f "$scripts_dir/${binary}.nu" ]]; then cp "$scripts_dir/${binary}.nu" "$config_scripts_dir/" || { log_warn "Failed to deploy ${binary}.nu" failed=$((failed + 1)) } && { deployed=$((deployed + 1)) chmod 755 "$config_scripts_dir/${binary}.nu" log_info " ✅ Deployed ${binary}.nu" } fi # Deploy binary library script (e.g., syntaxis-cli-lib.nu) if [[ -f "$scripts_dir/${binary}-lib.nu" ]]; then cp "$scripts_dir/${binary}-lib.nu" "$config_scripts_dir/" || { log_warn "Failed to deploy ${binary}-lib.nu" failed=$((failed + 1)) } && { deployed=$((deployed + 1)) chmod 755 "$config_scripts_dir/${binary}-lib.nu" log_info " ✅ Deployed ${binary}-lib.nu" } fi done if [[ $deployed -gt 0 ]]; then log_success "Wrapper scripts deployed ($deployed files)" return 0 else log_warn "No wrapper scripts found in bundle" return 0 fi } # Deploy configuration files deploy_configs() { local config_dir="${CONFIG_DIR:-$HOME/.config/syntaxis}" log_info "Deploying configuration files to $config_dir..." if [[ ! -d "$BUNDLE_DIR/configs" ]]; then log_warn "No configs directory found in bundle" return 0 fi if ! ensure_directory "$config_dir" "config directory"; then return 1 fi local config_count=0 # Copy all config files while IFS= read -r -d '' config_file; do if [[ -f "$config_file" ]]; then local relative="${config_file#$BUNDLE_DIR/configs/}" local dest_path="$config_dir/$relative" local dest_dir=$(dirname "$dest_path") # Create subdirectories mkdir -p "$dest_dir" || return 1 # Check if file exists if [[ -f "$dest_path" ]] && [[ $FORCE == false ]]; then log_info "Config already exists: $relative (keeping original)" continue fi cp "$config_file" "$dest_path" || { log_error "Failed to deploy config: $relative" continue } log_success "Deployed: $relative" config_count=$((config_count + 1)) fi done < <(find "$BUNDLE_DIR/configs" -type f -print0) if [[ $config_count -gt 0 ]]; then log_success "Deployed $config_count configuration file(s)" fi return 0 } # Create installation manifest create_manifest() { local manifest_dir="$HOME/.syntaxis" local manifest_file="$manifest_dir/manifest.toml" mkdir -p "$manifest_dir" || return 1 local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") local config_dir="${CONFIG_DIR:-$HOME/.config/syntaxis}" cat > "$manifest_file" << EOF [installation] timestamp = "$timestamp" prefix = "$(eval echo ~)/.local" version = "0.1.0" bundle = "$BUNDLE_NAME" [[binaries]] name = "syntaxis-cli" installed_at = "$timestamp" [[binaries]] name = "syntaxis-tui" installed_at = "$timestamp" [[binaries]] name = "syntaxis-api" installed_at = "$timestamp" [deployment] config_dir = "$config_dir" deployed_at = "$timestamp" EOF log_success "Manifest saved: $manifest_file" return 0 } # Print section header print_header() { local title="$1" echo "" echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BLUE}${title}${NC}" echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" } # Show installation summary show_summary() { echo "" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${GREEN}✅ Installation Complete!${NC}" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo "Installation Details:" echo " Prefix: $INSTALL_PREFIX" echo " Binaries: $BIN_DIR" echo " Configs: ${CONFIG_DIR:-$HOME/.config/syntaxis}" echo " Manifest: $HOME/.syntaxis/manifest.toml" echo "" echo "Quick Start:" echo " syntaxis-cli --help # CLI documentation" echo " syntaxis-tui # Launch terminal UI" echo " syntaxis-api # Start API server (port 3000)" echo "" echo "Next Steps:" echo " 1. Ensure $BIN_DIR is in your PATH" echo " 2. Reload your shell: source ~/.bashrc (or ~/.zshrc)" echo " 3. Verify: syntaxis-cli --version" echo "" echo "Configuration:" echo " Edit configs in: ${CONFIG_DIR:-$HOME/.config/syntaxis}/" echo " Set environment: export SYNTAXIS_CONFIG_DIR=${CONFIG_DIR:-$HOME/.config/syntaxis}" echo "" } # Main installation flow main() { parse_args "$@" if [[ $HELP == true ]]; then show_help exit 0 fi # Welcome message echo -e "${BLUE}" echo "╔════════════════════════════════════════════════════════╗" echo "║ syntaxis Bundle Installer ║" echo "║ Offline Installation Tool ║" echo "╚════════════════════════════════════════════════════════╝" echo -e "${NC}" log "Starting installation from: $BUNDLE_DIR" # Validate bundle if ! validate_bundle; then log_error "Bundle validation failed" exit 1 fi # Verify checksums if requested if [[ $VERIFY == true ]]; then if ! verify_checksums; then if [[ $FORCE == false ]]; then log_error "Checksum verification failed, aborting installation" exit 1 else log_warn "Checksum verification failed, but continuing (--force)" fi fi fi # Detect installation prefix detect_install_prefix # Show configuration echo "" echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BLUE}Installation Configuration${NC}" echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo " Installation Prefix: $INSTALL_PREFIX" echo " Binaries Directory: $BIN_DIR" echo " Config Directory: ${CONFIG_DIR:-$HOME/.config/syntaxis}" echo " Backup Existing: $BACKUP" echo " Verify Checksums: $VERIFY" echo " Force Overwrite: $FORCE" echo " Dry Run: $DRY_RUN" echo "" if [[ $DRY_RUN == true ]]; then log_info "DRY RUN MODE - No changes will be made" return 0 fi # Confirm installation (unless unattended) if [[ $UNATTENDED == false ]]; then echo -n "Continue with installation? (y/n) " read -r response if [[ $response != [yY] ]]; then log "Installation cancelled" exit 0 fi fi echo "" # Ensure directories exist and are writable if ! ensure_directory "$BIN_DIR" "installation directory"; then log_error "Cannot write to installation directory" exit 1 fi # Check NuShell availability and inform user print_header "Wrapper Configuration" if check_nushell; then log_success "NuShell detected ($NUSHELL_VERSION)" log_info "Wrappers will use full-featured mode with NuShell libraries" else log_warn "NuShell not detected" log_info "Wrappers will use simplified mode (config auto-discovery only)" prompt_nushell # This may exit or continue fi # Install binaries if ! install_binaries; then log_error "Binary installation failed" exit 1 fi # Deploy wrapper scripts (for auto-config injection) if ! deploy_wrapper_scripts; then log_warn "Wrapper scripts deployment failed (continuing - wrappers optional)" fi # Deploy configs if ! deploy_configs; then log_warn "Configuration deployment failed (continuing)" fi # Create manifest if ! create_manifest; then log_warn "Failed to create manifest" fi # Show summary show_summary log_success "Installation successful" } # Run main function main "$@"