Jesús Pérez 9cef9b8d57 refactor: consolidate configuration directories
Merge _configs/ into config/ for single configuration directory.
Update all path references.

Changes:
- Move _configs/* to config/
- Update .gitignore for new patterns
- No code references to _configs/ found

Impact: -1 root directory (layout_conventions.md compliance)
2025-12-26 18:36:23 +00:00

879 lines
25 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 "$@"