879 lines
25 KiB
Bash
Raw Permalink Normal View History

#!/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 <PATH> Installation prefix
Default: /usr/local (Linux/macOS) or /Program Files/syntaxis (Windows)
--config-dir <PATH> 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 "$@"